Danish updates from David Munch
[adiumx.git] / Plugins / Purple Service / CBPurpleAccount.m
blob224ce49704cb74598ae04f654c00b334e380fc11
1 /* 
2  * Adium is the legal property of its developers, whose names are listed in the copyright file included
3  * with this source distribution.
4  * 
5  * This program is free software; you can redistribute it and/or modify it under the terms of the GNU
6  * General Public License as published by the Free Software Foundation; either version 2 of the License,
7  * or (at your option) any later version.
8  * 
9  * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
10  * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
11  * Public License for more details.
12  * 
13  * You should have received a copy of the GNU General Public License along with this program; if not,
14  * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
15  */
17 #import "CBPurpleAccount.h"
18 #import <libpurple/cmds.h>
19 #import <AdiumLibpurple/SLPurpleCocoaAdapter.h>
20 #import <Adium/AIAccount.h>
21 #import <Adium/AIChat.h>
22 #import <Adium/AIContentMessage.h>
23 #import <Adium/AIContentNotification.h>
24 #import <Adium/AIHTMLDecoder.h>
25 #import <Adium/AIListContact.h>
26 #import <Adium/AIListGroup.h>
27 #import <Adium/AIListObject.h>
28 #import <Adium/AIMetaContact.h>
29 #import <Adium/AIService.h>
30 #import <Adium/AIServiceIcons.h>
31 #import <Adium/AIStatus.h>
32 #import <Adium/ESFileTransfer.h>
33 #import <Adium/AIWindowController.h>
34 #import <Adium/AIEmoticon.h>
35 #import <Adium/AIAccountControllerProtocol.h>
36 #import <Adium/AIChatControllerProtocol.h>
37 #import <Adium/AIContactControllerProtocol.h>
38 #import <Adium/AIContentControllerProtocol.h>
39 #import <Adium/AIInterfaceControllerProtocol.h>
40 #import <Adium/AIStatusControllerProtocol.h>
41 #import <Adium/AIPreferenceControllerProtocol.h>
42 #import <AIUtilities/AIAttributedStringAdditions.h>
43 #import <AIUtilities/AIDictionaryAdditions.h>
44 #import <AIUtilities/AIMenuAdditions.h>
45 #import <AIUtilities/AIMutableOwnerArray.h>
46 #import <AIUtilities/AIStringAdditions.h>
47 #import <AIUtilities/AIApplicationAdditions.h>
48 #import <AIUtilities/AIObjectAdditions.h>
49 #import <AIUtilities/AIImageAdditions.h>
50 #import <AIUtilities/AISystemNetworkDefaults.h>
51 #import "ESiTunesPlugin.h"
52 #import "AMPurpleTuneTooltip.h"
53 #import "adiumPurpleRequest.h"
55 #import "ESMSNService.h" //why oh why must the superclass know about MSN specific things!?
57 #define NO_GROUP                                                @"__NoGroup__"
59 #define PREF_GROUP_ALIASES                      @"Aliases"              //Preference group to store aliases in
60 #define NEW_ACCOUNT_DISPLAY_TEXT                AILocalizedString(@"<New Account>", "Placeholder displayed as the name of a new account")
62 @interface CBPurpleAccount (PRIVATE)
63 - (NSString *)_mapIncomingGroupName:(NSString *)name;
64 - (NSString *)_mapOutgoingGroupName:(NSString *)name;
66 - (void)setTypingFlagOfChat:(AIChat *)inChat to:(NSNumber *)typingState;
68 - (void)_receivedMessage:(NSAttributedString *)attributedMessage inChat:(AIChat *)chat fromListContact:(AIListContact *)sourceContact flags:(PurpleMessageFlags)flags date:(NSDate *)date;
69 - (void)_sentMessage:(NSAttributedString *)attributedMessage inChat:(AIChat *)chat toDestinationListContact:(AIListContact *)destinationContact flags:(PurpleMessageFlags)flags date:(NSDate *)date;
70 - (NSString *)_messageImageCachePathForID:(int)imageID;
72 - (ESFileTransfer *)createFileTransferObjectForXfer:(PurpleXfer *)xfer;
74 - (NSNumber *)shouldCheckMail;
76 - (void)configurePurpleAccountNotifyingTarget:(id)target selector:(SEL)selector;
77 - (void)continueConnectWithConfiguredPurpleAccount;
78 - (void)continueConnectWithConfiguredProxy;
79 - (void)continueRegisterWithConfiguredPurpleAccount;
80 - (void)promptForHostBeforeConnecting;
82 - (void)setAccountProfileTo:(NSAttributedString *)profile configurePurpleAccountContext:(NSInvocation *)inInvocation;
84 - (void)performAccountMenuAction:(NSMenuItem *)sender;
85 @end
87 @implementation CBPurpleAccount
89 static SLPurpleCocoaAdapter *purpleThread = nil;
91 // The PurpleAccount currently associated with this Adium account
92 - (PurpleAccount*)purpleAccount
94         //Create a purple account if one does not already exist
95         if (!account) {
96                 [self createNewPurpleAccount];
97                 AILog(@"%x: created PurpleAccount 0x%x with UID %@, protocolPlugin %s", [NSRunLoop currentRunLoop],account, [self UID], [self protocolPlugin]);
98         }
99         
100     return account;
103 - (SLPurpleCocoaAdapter *)purpleThread
105         return purpleThread;
108 // Subclasses must override this
109 - (const char*)protocolPlugin { return NULL; }
111 // Contacts ------------------------------------------------------------------------------------------------
112 #pragma mark Contacts
113 - (void)newContact:(AIListContact *)theContact withName:(NSString *)inName
118 - (void)updateContact:(AIListContact *)theContact toGroupName:(NSString *)groupName contactName:(NSString *)contactName
120         //A quick sign on/sign off can leave these messages in the threaded messaging queue... we most definitely don't want
121         //to put the contact back into a remote group after signing off, as a ghost will appear. Spooky!
122         if ([self online] || [self integerStatusObjectForKey:@"Connecting"]) {
123                 //When a new contact is created, if we aren't already silent and delayed, set it  a second to cover our initial
124                 //status updates
125                 if (!silentAndDelayed) {
126                         [self silenceAllContactUpdatesForInterval:2.0];
127                         [[adium contactController] delayListObjectNotificationsUntilInactivity];                
128                 }
130                 //If the name we were passed differs from the current formatted UID of the contact, it's itself a formatted UID
131                 //This is important since we may get an alias ("Evan Schoenberg") from the server but also want the formatted name
132                 if (![contactName isEqualToString:[theContact formattedUID]] && ![contactName isEqualToString:[theContact UID]]) {
133                         [theContact setStatusObject:contactName
134                                                                  forKey:@"FormattedUID"
135                                                                  notify:NotifyLater];
136                 }
138                 if (groupName && [groupName isEqualToString:@PURPLE_ORPHANS_GROUP_NAME]) {
139                         [theContact setRemoteGroupName:AILocalizedString(@"Orphans","Name for the orphans group")];
140                 } else if (groupName && [groupName length] != 0) {
141                         [theContact setRemoteGroupName:[self _mapIncomingGroupName:groupName]];
142                 } else {
143                         AILog(@"Got a nil group for %@",theContact);
144                 }
145                 
146                 [self gotGroupForContact:theContact];
147         } else {
148                 AILog(@"Got %@ for %@ while not online",groupName,theContact);
149         }
153  * @brief Change the UID of a contact
155  * If we're just passed a formatted version of the current UID, don't change the UID but instead use the information
156  * as the FormattedUID.  For example, we get sent this when an AIM contact's name formatting changes; we always want
157  * to use a lowercase and space-free version for the UID, however.
158  */
159 - (void)renameContact:(AIListContact *)theContact toUID:(NSString *)newUID
161         //If the name we were passed differs from the current formatted UID of the contact, it's itself a formatted UID
162         //This is important since we may get an alias ("Evan Schoenberg") from the server but also want the formatted name
163         NSString        *filteredUID = [[self service] filterUID:newUID removeIgnoredCharacters:YES];
164         
165         if ([filteredUID isEqualToString:[theContact UID]]) {
166                 [theContact setStatusObject:newUID
167                                                          forKey:@"FormattedUID"
168                                                          notify:NotifyLater];           
169         } else {
170                 [theContact setUID:newUID];             
171         }
174 - (void)updateContact:(AIListContact *)theContact toAlias:(NSString *)purpleAlias
176         if (![[purpleAlias compactedString] isEqualToString:[[theContact UID] compactedString]]) {
177                 //Store this alias as the serverside display name so long as it isn't identical when unformatted to the UID
178                 [theContact setServersideAlias:purpleAlias
179                                            asStatusMessage:[self useDisplayNameAsStatusMessage]
180                                                           silently:silentAndDelayed];
182         } else {
183                 //If it's the same characters as the UID, apply it as a formatted UID
184                 if (![purpleAlias isEqualToString:[theContact formattedUID]] && 
185                         ![purpleAlias isEqualToString:[theContact UID]]) {
186                         [theContact setFormattedUID:purpleAlias
187                                                                  notify:NotifyLater];
189                         //Apply any changes
190                         [theContact notifyOfChangedStatusSilently:silentAndDelayed];
191                 }
192         }
195 - (BOOL)useDisplayNameAsStatusMessage
197         return NO;
200 - (void)updateContact:(AIListContact *)theContact forEvent:(NSNumber *)event
202 }               
205 //Signed online
206 - (void)updateSignon:(AIListContact *)theContact withData:(void *)data
208         [theContact setOnline:YES
209                                    notify:NotifyLater
210                                  silently:silentAndDelayed];
212         [theContact notifyOfChangedStatusSilently:silentAndDelayed];
215 //Signed offline
216 - (void)updateSignoff:(AIListContact *)theContact withData:(void *)data
218         [theContact setOnline:NO
219                                    notify:NotifyLater
220                                  silently:silentAndDelayed];
221         
222         [theContact notifyOfChangedStatusSilently:silentAndDelayed];
225 //Signon Time
226 - (void)updateSignonTime:(AIListContact *)theContact withData:(NSDate *)signonDate
227 {       
228         [theContact setSignonDate:signonDate
229                                            notify:NotifyLater];
230         
231         //Apply any changes
232         [theContact notifyOfChangedStatusSilently:silentAndDelayed];
236  * @brief Status name to use for a Purple buddy
237  */
238 - (NSString *)statusNameForPurpleBuddy:(PurpleBuddy *)buddy
240         return nil;
244  * @brief Status message for a contact
245  */
246 - (NSAttributedString *)statusMessageForPurpleBuddy:(PurpleBuddy *)buddy
248         PurplePresence          *presence = purple_buddy_get_presence(buddy);
249         PurpleStatus            *status = (presence ? purple_presence_get_active_status(presence) : NULL);
250         const char                      *message = (status ? purple_status_get_attr_string(status, "message") : NULL);
251         
252         return (message ? [AIHTMLDecoder decodeHTML:[NSString stringWithUTF8String:message]] : nil);
256  * @brief Update the status message and away state of the contact
257  */
258 - (void)updateStatusForContact:(AIListContact *)theContact toStatusType:(NSNumber *)statusTypeNumber statusName:(NSString *)statusName statusMessage:(NSAttributedString *)statusMessage
260         [theContact setStatusWithName:statusName
261                                            statusType:[statusTypeNumber intValue]
262                                                    notify:NotifyLater];
263         [theContact setStatusMessage:statusMessage
264                                                   notify:NotifyLater];
265         
266         //Apply the change
267         [theContact notifyOfChangedStatusSilently:silentAndDelayed];
270 //Idle time
271 - (void)updateWentIdle:(AIListContact *)theContact withData:(NSDate *)idleSinceDate
273         [theContact setIdle:YES sinceDate:idleSinceDate notify:NotifyLater];
275         //Apply any changes
276         [theContact notifyOfChangedStatusSilently:silentAndDelayed];
278 - (void)updateIdleReturn:(AIListContact *)theContact withData:(void *)data
280         [theContact setIdle:NO
281                           sinceDate:nil
282                                  notify:NotifyLater];
284         //Apply any changes
285         [theContact notifyOfChangedStatusSilently:silentAndDelayed];
287         
288 //Evil level (warning level)
289 - (void)updateEvil:(AIListContact *)theContact withData:(NSNumber *)evilNumber
291         [theContact setWarningLevel:[evilNumber intValue]
292                                                  notify:NotifyLater];
294         //Apply any changes
295         [theContact notifyOfChangedStatusSilently:silentAndDelayed];
296 }   
298 //Buddy Icon
299 - (void)updateIcon:(AIListContact *)theContact withData:(NSData *)userIconData
301         [theContact setServersideIconData:userIconData
302                                                            notify:NotifyLater];
304         //Apply any changes
305         [theContact notifyOfChangedStatusSilently:silentAndDelayed];
308 - (void)updateMobileStatus:(AIListContact *)theContact withData:(BOOL)isMobile
310         [theContact setIsMobile:isMobile notify:NotifyLater];
312         [theContact notifyOfChangedStatusSilently:silentAndDelayed];
315 - (NSString *)processedIncomingUserInfo:(NSString *)inString
317         NSMutableString *returnString = nil;
318         if ([inString rangeOfString:@"Purple could not find any information in the user's profile. The user most likely does not exist."].location != NSNotFound) {
319                 returnString = [[inString mutableCopy] autorelease];
320                 [returnString replaceOccurrencesOfString:@"Purple could not find any information in the user's profile. The user most likely does not exist."
321                                                                           withString:AILocalizedString(@"Adium could not find any information in the user's profile. This may not be a registered name.", "Message shown when a contact's profile can't be found")
322                                                                                  options:NSLiteralSearch
323                                                                                    range:NSMakeRange(0, [returnString length])];
324         }
325         
326         return (returnString ? returnString : inString);
330 - (void)updateUserInfo:(AIListContact *)theContact withData:(PurpleNotifyUserInfo *)user_info
332         char *user_info_text = purple_notify_user_info_get_text_with_newline(user_info, "<BR />");
333         NSMutableString *mutablePurpleUserInfo = (user_info_text ? [NSMutableString stringWithUTF8String:user_info_text] : nil);
334         g_free(user_info_text);
336         //Libpurple may pass us HTML with embedded </html> tags. Yuck. Don't abort when we hit one in AIHTMLDecoder.
337         [mutablePurpleUserInfo replaceOccurrencesOfString:@"</html>"
338                                                                                  withString:@""
339                                                                                         options:(NSCaseInsensitiveSearch | NSLiteralSearch)
340                                                                                           range:NSMakeRange(0, [mutablePurpleUserInfo length])];
342         NSString        *purpleUserInfo = mutablePurpleUserInfo;
343         purpleUserInfo = processPurpleImages(purpleUserInfo, self);
344         purpleUserInfo = [self processedIncomingUserInfo:purpleUserInfo];
346         [theContact setProfile:[AIHTMLDecoder decodeHTML:purpleUserInfo]
347                                         notify:NotifyLater];
349         //Apply any changes
350         [theContact notifyOfChangedStatusSilently:silentAndDelayed];
354  * @brief Purple removed a contact from the local blist
356  * This can happen in many situations:
357  *      - For every contact on an account when the account signs off
358  *      - For a contact as it is deleted by the user
359  *      - For a contact as it is deleted by Purple (e.g. when Sametime refuses an addition because it is known to be invalid)
360  *      - In the middle of the move process as a contact moves from one group to another
362  * We need not take any action; we'll be notified of changes by Purple as necessary.
363  */
364 - (void)removeContact:(AIListContact *)theContact
369 //To allow root level buddies on protocols which don't support them, we map any buddies in a group
370 //named after this account's UID to the root group.  These functions handle the mapping.  Group names should
371 //be filtered through incoming before being sent to Adium - and group names from Adium should be filtered through
372 //outgoing before being used.
373 - (NSString *)_mapIncomingGroupName:(NSString *)name
375         if (!name || ([[name compactedString] caseInsensitiveCompare:[self UID]] == NSOrderedSame)) {
376                 return ADIUM_ROOT_GROUP_NAME;
377         } else {
378                 return name;
379         }
381 - (NSString *)_mapOutgoingGroupName:(NSString *)name
383         if ([[name compactedString] caseInsensitiveCompare:ADIUM_ROOT_GROUP_NAME] == NSOrderedSame) {
384                 return [self UID];
385         } else {
386                 return name;
387         }
390 //Update the status of a contact (Request their profile)
391 - (void)delayedUpdateContactStatus:(AIListContact *)inContact
393     //Request profile
394         [purpleThread getInfoFor:[inContact UID] onAccount:self];
397 - (void)requestAddContactWithUID:(NSString *)contactUID
399         [[adium contactController] requestAddContactWithUID:contactUID
400                                                                                                 service:[self _serviceForUID:contactUID]
401                                                                                                 account:self];
404 - (AIService *)_serviceForUID:(NSString *)contactUID
406         return [self service];
409 - (void)gotGroupForContact:(AIListContact *)listContact {};
411 /*********************/
412 /* AIAccount_Handles */
413 /*********************/
414 #pragma mark Contact List Editing
416 - (void)removeContacts:(NSArray *)objects
418         NSEnumerator    *enumerator = [objects objectEnumerator];
419         AIListContact   *object;
420         
421         while ((object = [enumerator nextObject])) {
422                 NSString        *groupName = [self _mapOutgoingGroupName:[object remoteGroupName]];
424                 //Have the purple thread perform the serverside actions
425                 [purpleThread removeUID:[object UID] onAccount:self fromGroup:groupName];
426                 
427                 //Remove it from Adium's list
428                 [object setRemoteGroupName:nil];
429         }
432 - (void)addContacts:(NSArray *)objects toGroup:(AIListGroup *)group
434         NSEnumerator    *enumerator = [objects objectEnumerator];
435         AIListContact   *object;
436         NSString                *groupName = [self _mapOutgoingGroupName:[group UID]];
437         
438         while ((object = [enumerator nextObject])) {
439                 [purpleThread addUID:[self _UIDForAddingObject:object] onAccount:self toGroup:groupName];
440                 
441                 //Add it to Adium's list
442                 [object setRemoteGroupName:[group UID]]; //Use the non-mapped group name locally
443         }
446 - (NSString *)_UIDForAddingObject:(AIListContact *)object
448         return [object UID];
451 - (void)moveListObjects:(NSArray *)objects toGroup:(AIListGroup *)group
453         NSString                *groupName = [self _mapOutgoingGroupName:[group UID]];
454         NSEnumerator    *enumerator;
455         AIListContact   *listObject;
456         
457         //Move the objects to it
458         enumerator = [objects objectEnumerator];
459         while ((listObject = [enumerator nextObject])) {
460                 if ([listObject isKindOfClass:[AIListGroup class]]) {
461                         //Since no protocol here supports nesting, a group move is really a re-name
462                         
463                 } else {
464                         //                      NSString        *oldGroupName = [self _mapOutgoingGroupName:[listObject remoteGroupName]];
465                         
466                         //Tell the purple thread to perform the serverside operation
467                         [purpleThread moveUID:[listObject UID] onAccount:self toGroup:groupName];
469                         //Use the non-mapped group name locally
470                         [listObject setRemoteGroupName:[group UID]];
471                 }
472         }               
475 - (void)renameGroup:(AIListGroup *)inGroup to:(NSString *)newName
477         NSString                *groupName = [self _mapOutgoingGroupName:[inGroup UID]];
479         //Tell the purple thread to perform the serverside operation    
480         [purpleThread renameGroup:groupName onAccount:self to:newName];
482         //We must also update the remote grouping of all our contacts in that group
483         NSEnumerator    *enumerator = [[[adium contactController] allContactsInObject:inGroup recurse:YES onAccount:self] objectEnumerator];
484         AIListContact   *contact;
485         
486         while ((contact = [enumerator nextObject])) {
487                 //Evan: should we use groupName or newName here?
488                 [contact setRemoteGroupName:newName];
489         }
492 - (void)deleteGroup:(AIListGroup *)inGroup
494         NSString                *groupName = [self _mapOutgoingGroupName:[inGroup UID]];
496         [purpleThread deleteGroup:groupName onAccount:self];
499 // Return YES if the contact list is editable
500 - (BOOL)contactListEditable
502     return [self online];
505 - (id)authorizationRequestWithDict:(NSDictionary*)dict {
506         return [[[AIObject sharedAdiumInstance] contactController] showAuthorizationRequestWithDict:dict
507                                                                                                                                                                          forAccount:self];
510 - (void)authorizationWindowController:(NSWindowController *)inWindowController authorizationWithDict:(NSDictionary *)infoDict didAuthorize:(BOOL)inDidAuthorize
512         id               callback;
514         if (account) {
515                 if (inDidAuthorize) {
516                         callback = [[[infoDict objectForKey:@"authorizeCB"] retain] autorelease];
517                 } else {
518                         callback = [[[infoDict objectForKey:@"denyCB"] retain] autorelease];
519                 }
520                 
521                 [purpleThread doAuthRequestCbValue:callback withUserDataValue:[[[infoDict objectForKey:@"userData"] retain] autorelease]];
522         }
525 //Chats ------------------------------------------------------------
526 #pragma mark Chats
529  * @brief Called by Purple code when a chat should be opened by the interface
531  * If the user sent an initial message, this will be triggered and have no effect.
533  * If a remote user sent an initial message, however, a chat will be created without being opened.  This call is our
534  * cue to actually open chat.
536  * Another situation in which this is relevant is when we request joining a group chat; the chat should only be actually
537  * opened once the server notifies us that we are in the room.
539  * This will ultimately call -[CBPurpleAccount openChat:] below if the chat was not previously open.
540  */
541 - (void)addChat:(AIChat *)chat
543         AILogWithSignature(@"");
545         //Open the chat
546         [[adium interfaceController] openChat:chat];
547         
548         [chat accountDidJoinChat];
551 //Open a chat for Adium
552 - (BOOL)openChat:(AIChat *)chat
554         /* The #if 0'd block below causes crashes in msn_tooltip_text() on MSN */
555 #if 0
556         AIListContact   *listContact;
557         
558         //Obtain the contact's information if it's a stranger
559         if ((listContact = [chat listObject]) && ([listContact isStranger])) {
560                 [self delayedUpdateContactStatus:listContact];
561         }
562 #endif
563         
564         AILog(@"purple openChat:%@ for %@",chat,[chat uniqueChatID]);
566         //Inform purple that we have opened this chat
567         [purpleThread openChat:chat onAccount:self];
568         
569         //Created the chat successfully
570         return YES;
573 - (BOOL)closeChat:(AIChat*)chat
575         [purpleThread closeChat:chat];
576         
577         //Be sure any remaining typing flag is cleared as the chat closes
578         [self setTypingFlagOfChat:chat to:nil];
579         AILog(@"purple closeChat:%@",[chat uniqueChatID]);
580         
581     return YES;
584 - (AIChat *)chatWithContact:(AIListContact *)contact identifier:(id)identifier
586         AIChat *chat = [[adium chatController] chatWithContact:contact];
587         [chat setIdentifier:identifier];
589         return chat;
593 - (AIChat *)chatWithName:(NSString *)name identifier:(id)identifier
595         return [[adium chatController] chatWithName:name identifier:identifier onAccount:self chatCreationInfo:nil];
598 //Typing update in an IM
599 - (void)typingUpdateForIMChat:(AIChat *)chat typing:(NSNumber *)typingState
601         [self setTypingFlagOfChat:chat
602                                                    to:typingState];
605 //Multiuser chat update
606 - (void)convUpdateForChat:(AIChat *)chat type:(NSNumber *)type
612  * @brief Called when we are informed that we left a multiuser chat
613  */
614 - (void)leftChat:(AIChat *)chat
616         AILogWithSignature(@"Chat left - something should happen here!");
619 - (void)updateTopic:(NSString *)inTopic forChat:(AIChat *)chat
621         
623 - (void)updateTitle:(NSString *)inTitle forChat:(AIChat *)chat
625         [[chat displayArrayForKey:@"Display Name"] setObject:inTitle
626                                                                                            withOwner:self];
629 - (void)updateForChat:(AIChat *)chat type:(NSNumber *)type
631         AIChatUpdateType        updateType = [type intValue];
632         NSString                        *key = nil;
633         switch (updateType) {
634                 case AIChatTimedOut:
635                 case AIChatClosedWindow:
636                         break;
637         }
638         
639         if (key) {
640                 [chat setStatusObject:[NSNumber numberWithBool:YES] forKey:key notify:NotifyNow];
641                 [chat setStatusObject:nil forKey:key notify:NotifyNever];
642                 
643         }
646 - (void)errorForChat:(AIChat *)chat type:(NSNumber *)type
648         [chat receivedError:type];
651 - (void)receivedIMChatMessage:(NSDictionary *)messageDict inChat:(AIChat *)chat
653         PurpleMessageFlags              flags = [[messageDict objectForKey:@"PurpleMessageFlags"] intValue];
654         
655         if ((flags & PURPLE_MESSAGE_SEND) != 0) {
656         //Purple is telling us that our message was sent successfully.
658                 //We would tell the other side that we're done typing, except that if we do this now, the typing notification icon in some clients (e.g., iChat) disappears before the message actually arrives.
659                 //[purpleThread sendTyping:AINotTyping inChat:chat];
660     } else {
661                 NSAttributedString              *attributedMessage;
662                 AIListContact                   *listContact;
663                 
664                 listContact = [chat listObject];
666                 attributedMessage = [[adium contentController] decodedIncomingMessage:[messageDict objectForKey:@"Message"]
667                                                                                                                                   fromContact:listContact
668                                                                                                                                         onAccount:self];
669                 
670                 //Clear the typing flag of the chat since a message was just received
671                 [self setTypingFlagOfChat:chat to:nil];
672                 
673                 [self _receivedMessage:attributedMessage
674                                                 inChat:chat 
675                            fromListContact:listContact
676                                                  flags:flags
677                                                   date:[messageDict objectForKey:@"Date"]];
678         }
681 - (void)receivedMultiChatMessage:(NSDictionary *)messageDict inChat:(AIChat *)chat
682 {       
683         PurpleMessageFlags      flags = [[messageDict objectForKey:@"PurpleMessageFlags"] intValue];
684         NSAttributedString      *attributedMessage = [messageDict objectForKey:@"AttributedMessage"];;
685         NSString                        *source = [messageDict objectForKey:@"Source"];
687         if (source) {
688                 [self _receivedMessage:attributedMessage
689                                                 inChat:chat 
690                            fromListContact:[self contactWithUID:source]
691                                                  flags:flags
692                                                   date:[messageDict objectForKey:@"Date"]];
693         } else {
694                 //If we didn't get a listContact, this is a purple status message... display it as such.
695 #warning need to translate the type here
696                 [[adium contentController] displayEvent:[attributedMessage string]
697                                                                                  ofType:@"purple"
698                                                                                  inChat:chat];
699                 
700         }
703 - (void)_receivedMessage:(NSAttributedString *)attributedMessage inChat:(AIChat *)chat fromListContact:(AIListContact *)sourceContact flags:(PurpleMessageFlags)flags date:(NSDate *)date
705         AIContentMessage *messageObject = [AIContentMessage messageInChat:chat
706                                                                                                                    withSource:sourceContact
707                                                                                                                   destination:self
708                                                                                                                                  date:date
709                                                                                                                           message:attributedMessage
710                                                                                                                         autoreply:(flags & PURPLE_MESSAGE_AUTO_RESP) != 0];
711         
712         [[adium contentController] receiveContentObject:messageObject];
715 /*********************/
716 /* AIAccount_Content */
717 /*********************/
718 #pragma mark Content
719 - (void)sendTypingObject:(AIContentTyping *)inContentTyping
721         AIChat *chat = [inContentTyping chat];
723         if (![chat isGroupChat]) {
724                 [purpleThread sendTyping:[inContentTyping typingState] inChat:chat];
725         }
728 - (BOOL)supportsSendingNotifications
730         return (account ? ((PURPLE_PLUGIN_PROTOCOL_INFO(purple_find_prpl(purple_account_get_protocol_id(account)))->send_attention) != NULL) : NO);
733 - (BOOL)sendMessageObject:(AIContentMessage *)inContentMessage
735         PurpleMessageFlags              flags = PURPLE_MESSAGE_RAW;
736         
737         if ([inContentMessage isKindOfClass:[AIContentNotification class]] &&
738                 [self supportsSendingNotifications]) {
739                 //Send a notification directly if possible
740                 [purpleThread sendNotificationOfType:[(AIContentNotification *)inContentMessage notificationType]
741                                                                  fromAccount:self
742                                                                           inChat:[inContentMessage chat]];
744         } else {
745                 if ([inContentMessage isAutoreply]) {
746                         flags |= PURPLE_MESSAGE_AUTO_RESP;
747                 }
749                 [purpleThread sendEncodedMessage:[inContentMessage encodedMessage]
750                                                          fromAccount:self
751                                                                   inChat:[inContentMessage chat]
752                                                            withFlags:flags];
753         }
754         
755         return YES;
759  * @brief Return the string encoded for sending to a remote contact
761  * We return nil if the string turns out to have been a / command.
762  */
763 - (NSString *)encodedAttributedStringForSendingContentMessage:(AIContentMessage *)inContentMessage
765         NSString        *encodedString;
766         BOOL            didCommand = [purpleThread attemptPurpleCommandOnMessage:[[inContentMessage message] string]
767                                                                                                                  fromAccount:(AIAccount *)[inContentMessage source]
768                                                                                                                           inChat:[inContentMessage chat]];      
769         
770         encodedString = (didCommand ?
771                                          nil :
772                                          [super encodedAttributedStringForSendingContentMessage:inContentMessage]);
774         return encodedString;
778  * @brief Allow newlines in messages
780  * Only IRC doesn't allow newlines out of the built-in prpls... and we don't even support it yet.
781  * This method is never called at present.
782  */
783 - (BOOL)allowsNewlinesInMessages
785         return (account && account->gc && ((account->gc->flags & PURPLE_CONNECTION_NO_NEWLINES) != 0));
789  * @brief Libpurple prints file transfer messages to the chat window. The Adium core therefore shouldn't.
790  */
791 - (BOOL)accountDisplaysFileTransferMessages
793         return YES;
796 //Return YES if we're available for sending the specified content or will be soon (are currently connecting).
797 //If inListObject is nil, we can return YES if we will 'most likely' be able to send the content.
798 - (BOOL)availableForSendingContentType:(NSString *)inType toContact:(AIListContact *)inContact
800     BOOL        weAreOnline = [self online];
801         
802     if ([inType isEqualToString:CONTENT_MESSAGE_TYPE] ||
803                 [inType isEqualToString:CONTENT_NOTIFICATION_TYPE]) {
804         if ((weAreOnline && (inContact == nil || [inContact online])) ||
805                         ([self integerStatusObjectForKey:@"Connecting"])) {  //XXX - Why do we lie if we're connecting? -ai
806                         return YES;
807         }
808     } else if (([inType isEqualToString:CONTENT_FILE_TRANSFER_TYPE]) && ([self conformsToProtocol:@protocol(AIAccount_Files)])) {
809                 if (weAreOnline) {
810                         if (inContact) {
811                                 if ([inContact online]) {
812                                         return [self allowFileTransferWithListObject:inContact];
813                                 }
814                         } else {
815                                 return YES;
816                         }
817        }        
818         }
819         
820     return NO;
823 - (BOOL)allowFileTransferWithListObject:(AIListObject *)inListObject
825         PurplePluginProtocolInfo *prpl_info = NULL;
827         if (account && account->gc && account->gc->prpl)
828                 prpl_info = PURPLE_PLUGIN_PROTOCOL_INFO(account->gc->prpl);
829         
830         if (prpl_info && prpl_info->send_file)
831                 return (!prpl_info->can_receive_file || prpl_info->can_receive_file(account->gc, [[inListObject UID] UTF8String]));
832         else
833                 return NO;
836 - (BOOL)supportsAutoReplies
838         if (account && account->gc) {
839                 return ((account->gc->flags & PURPLE_CONNECTION_AUTO_RESP) != 0);
840         }
841         
842         return NO;
845 - (BOOL)canSendOfflineMessageToContact:(AIListContact *)inContact
847         PurplePluginProtocolInfo *prpl_info = NULL;
849         if (account && account->gc && account->gc->prpl)
850                 prpl_info = PURPLE_PLUGIN_PROTOCOL_INFO(account->gc->prpl);
851         
852         if (prpl_info && prpl_info->offline_message) {
853                 
854                 return (prpl_info->offline_message(purple_find_buddy(account, [[inContact UID] UTF8String])));
856         } else
857                 return NO;
858         
861 #pragma mark Custom emoticons
862 - (void)chat:(AIChat *)inChat isWaitingOnCustomEmoticon:(NSString *)emoticonEquivalent
864         if(![[[adium preferenceController] preferenceForKey:KEY_MSN_DISPLAY_CUSTOM_EMOTICONS
865                                                                                                   group:PREF_GROUP_MSN_SERVICE] boolValue])
866                 return;
867         AIEmoticon *emoticon;
869         //Look for an existing emoticon with this equivalent
870         NSEnumerator *enumerator = [[inChat customEmoticons] objectEnumerator];
871         while ((emoticon = [enumerator nextObject])) {
872                 if ([[emoticon textEquivalents] containsObject:emoticonEquivalent]) break;
873         }
874         
875         if (!emoticon) {
876                 emoticon = [AIEmoticon emoticonWithIconPath:nil
877                                                                                 equivalents:[NSArray arrayWithObject:emoticonEquivalent]
878                                                                                            name:emoticonEquivalent
879                                                                                            pack:nil];
880                 [inChat addCustomEmoticon:emoticon];                    
881         }
882         
883         if (![emoticon path]) {
884                 [emoticon setPath:[[NSBundle bundleForClass:[CBPurpleAccount class]] pathForResource:@"missing_image"
885                                                                                                                                                                         ofType:@"png"]];
886         }
890  * @brief Return the path at which to save an emoticon
891  */
892 - (NSString *)_emoticonCachePathForEmoticon:(NSString *)emoticonEquivalent type:(AIBitmapImageFileType)fileType inChat:(AIChat *)inChat
894         static unsigned long long emoticonID = 0;
895     NSString    *filename = [NSString stringWithFormat:@"TEMP-CustomEmoticon_%@_%@_%qu.%@",
896                 [inChat uniqueChatID], emoticonEquivalent, emoticonID++, [NSImage extensionForBitmapImageFileType:fileType]];
897     return [[adium cachesPath] stringByAppendingPathComponent:[filename safeFilenameString]];   
901 - (void)chat:(AIChat *)inChat setCustomEmoticon:(NSString *)emoticonEquivalent withImageData:(NSData *)inImageData
903         if(![[[adium preferenceController] preferenceForKey:KEY_MSN_DISPLAY_CUSTOM_EMOTICONS
904                                                                                                   group:PREF_GROUP_MSN_SERVICE] boolValue])
905                 return;
906         /* XXX Note: If we can set outgoing emoticons, this method needs to be updated to mark emoticons as incoming
907          * and AIEmoticonController needs to be able to handle that.
908          */
909         AIEmoticon      *emoticon;
911         //Look for an existing emoticon with this equivalent
912         NSEnumerator *enumerator = [[inChat customEmoticons] objectEnumerator];
913         while ((emoticon = [enumerator nextObject])) {
914                 if ([[emoticon textEquivalents] containsObject:emoticonEquivalent]) break;
915         }
916         
917         //Write out our image
918         NSString        *path = [self _emoticonCachePathForEmoticon:emoticonEquivalent
919                                                                                                            type:[NSImage fileTypeOfData:inImageData]
920                                                                                                          inChat:inChat];
921         [inImageData writeToFile:path
922                                   atomically:NO];
924         if (emoticon) {
925                 //If we already have an emoticon, just update its path
926                 [emoticon setPath:path];
928         } else {
929                 emoticon = [AIEmoticon emoticonWithIconPath:path
930                                                                                 equivalents:[NSArray arrayWithObject:emoticonEquivalent]
931                                                                                            name:emoticonEquivalent
932                                                                                            pack:nil];
933                 [inChat addCustomEmoticon:emoticon];
934         }
937 - (void)chat:(AIChat *)inChat closedCustomEmoticon:(NSString *)emoticonEquivalent
939         if(![[[adium preferenceController] preferenceForKey:KEY_MSN_DISPLAY_CUSTOM_EMOTICONS
940                                                                                                   group:PREF_GROUP_MSN_SERVICE] boolValue])
941                 return;
942         AIEmoticon      *emoticon;
944         //Look for an existing emoticon with this equivalent
945         NSEnumerator *enumerator = [[inChat customEmoticons] objectEnumerator];
946         while ((emoticon = [enumerator nextObject])) {
947                 if ([[emoticon textEquivalents] containsObject:emoticonEquivalent]) break;
948         }
949         
950         if (emoticon) {
951                 [[adium notificationCenter] postNotificationName:@"AICustomEmoticonUpdated"
952                                                                                                   object:inChat
953                                                                                                 userInfo:[NSDictionary dictionaryWithObject:emoticon
954                                                                                                                                                                          forKey:@"AIEmoticon"]];
955         } else {
956                 //This shouldn't happen; chat:setCustomEmoticon:withImageData: should have already been called.
957                 emoticon = [AIEmoticon emoticonWithIconPath:nil
958                                                                                 equivalents:[NSArray arrayWithObject:emoticonEquivalent]
959                                                                                            name:emoticonEquivalent
960                                                                                            pack:nil];
961                 NSLog(@"Warning: closed custom emoticon %@ without adding it to the chat", emoticon);
962                 AILog(@"Warning: closed custom emoticon %@ without adding it to the chat", emoticon);
963         }
966 #pragma mark PurpleConversation User Lists
967 - (NSString *)uidForContactWithUID:(NSString *)inUID inChat:(AIChat *)chat
969         //No change for the superclass; subclasses may wish to modify it
970         return inUID;
972 - (void)addUsersArray:(NSArray *)usersArray
973                         withFlags:(NSArray *)flagsArray
974                    andAliases:(NSArray *)aliasesArray 
975                   newArrivals:(NSNumber *)newArrivals
976                            toChat:(AIChat *)chat
978         int                     i, count;
979         BOOL            isNewArrival = (newArrivals && [newArrivals boolValue]);
981         AILog(@"*** %@: addUsersArray:%@ toChat:%@",self,usersArray,chat);
983         count = [usersArray count];
984         for (i = 0; i < count; i++) {
985                 NSString                                *contactName;
986                 NSString                                *alias;
987                 AIListContact                   *listContact;
988                 PurpleConvChatBuddyFlags        flags;
990                 contactName = [usersArray objectAtIndex:i];
991                 flags = [[flagsArray objectAtIndex:i] intValue];
992                 alias = [aliasesArray objectAtIndex:i];
994                 listContact = [self contactWithUID:[self uidForContactWithUID:contactName inChat:chat]];
995                 [listContact setStatusObject:contactName forKey:@"FormattedUID" notify:NotifyNow];
997                 if (alias && [alias length]) {
998                         [listContact setServersideAlias:alias asStatusMessage:NO silently:YES];
999                 }
1001                 [chat addParticipatingListObject:listContact notify:isNewArrival];
1002         }
1005 - (void)removeUser:(NSString *)contactName fromChat:(AIChat *)chat
1007         AIListContact   *contact;
1009         if ((chat) && 
1010                 (contact = [self contactWithUID:[self uidForContactWithUID:contactName inChat:chat]])) {
1011                 
1012                 [chat removeObject:contact];
1013                 
1014                 AILog(@"%@ removeUser:%@ fromChat:%@",self,contact,chat);
1015         } else {
1016                 AILog(@"Could not remove %@ from %@ (contactWithUID: %@)",
1017                           contactName,chat,[self contactWithUID:[self uidForContactWithUID:contactName inChat:chat]]);
1018         }
1021 - (void)removeUsersArray:(NSArray *)usersArray fromChat:(AIChat *)chat
1023         NSEnumerator    *enumerator = [usersArray objectEnumerator];
1024         NSString                *contactName;
1025         while ((contactName = [enumerator nextObject])) {
1026                 [self removeUser:contactName fromChat:chat];
1027         }
1030 /*********************/
1031 /* AIAccount_Privacy */
1032 /*********************/
1033 #pragma mark Privacy
1034 - (BOOL)addListObject:(AIListObject *)inObject toPrivacyList:(AIPrivacyType)type
1036     if (type == AIPrivacyTypePermit)
1037         return (purple_privacy_permit_add(account,[[inObject UID] UTF8String],FALSE));
1038     else
1039         return (purple_privacy_deny_add(account,[[inObject UID] UTF8String],FALSE));
1042 - (BOOL)removeListObject:(AIListObject *)inObject fromPrivacyList:(AIPrivacyType)type
1044     if (type == AIPrivacyTypePermit)
1045         return (purple_privacy_permit_remove(account,[[inObject UID] UTF8String],FALSE));
1046     else
1047         return (purple_privacy_deny_remove(account,[[inObject UID] UTF8String],FALSE));
1050 - (NSArray *)listObjectsOnPrivacyList:(AIPrivacyType)type
1052         NSMutableArray  *array = [NSMutableArray array];
1053         if (account) {
1054                 GSList                  *list;
1055                 GSList                  *sourceList = ((type == AIPrivacyTypePermit) ? account->permit : account->deny);
1056                 
1057                 for (list = sourceList; (list != NULL); list=list->next) {
1058                         [array addObject:[self contactWithUID:[NSString stringWithUTF8String:(char *)list->data]]];
1059                 }
1060         }
1062         return array;
1065 - (void)accountPrivacyList:(AIPrivacyType)type added:(NSString *)sourceUID
1067         //Can't really trust sourceUID to not be @"" or something silly like that
1068         if ([sourceUID length]) {
1069                 //Get our contact
1070                 AIListContact   *contact = [self contactWithUID:sourceUID];
1072                 //Update Adium's knowledge of it
1073                 [contact setIsBlocked:((type == AIPrivacyTypeDeny) ? YES : NO) updateList:NO];
1074         }
1077 - (void)privacyPermitListAdded:(NSString *)sourceUID
1079         [self accountPrivacyList:AIPrivacyTypePermit added:sourceUID];
1082 - (void)privacyDenyListAdded:(NSString *)sourceUID
1084         [self accountPrivacyList:AIPrivacyTypeDeny added:sourceUID];
1087 - (void)accountPrivacyList:(AIPrivacyType)type removed:(NSString *)sourceUID
1089         //Can't really trust sourceUID to not be @"" or something silly like that
1090         if ([sourceUID length]) {
1091                 if (!namesAreCaseSensitive) {
1092                         sourceUID = [sourceUID compactedString];
1093                 }
1095                 //Get our contact, which must already exist for us to care about its removal
1096                 AIListContact   *contact = [[adium contactController] existingContactWithService:service
1097                                                                                                                                                                  account:self
1098                                                                                                                                                                          UID:sourceUID];
1099                 
1100                 if (contact) {                  
1101                         //Update Adium's knowledge of it
1102                         [contact setIsBlocked:((type == AIPrivacyTypeDeny) ? NO : YES) updateList:NO];
1103                 }
1104         }
1107 - (void)privacyPermitListRemoved:(NSString *)sourceUID
1109         [self accountPrivacyList:AIPrivacyTypePermit removed:sourceUID];
1112 - (void)privacyDenyListRemoved:(NSString *)sourceUID
1114         [self accountPrivacyList:AIPrivacyTypeDeny removed:sourceUID];
1117 - (void)setPrivacyOptions:(AIPrivacyOption)option
1119         if (account && purple_account_get_connection(account)) {
1120                 PurplePrivacyType privacyType;
1122                 switch (option) {
1123                         case AIPrivacyOptionAllowAll:
1124                         default:
1125                                 privacyType = PURPLE_PRIVACY_ALLOW_ALL;
1126                                 break;
1127                         case AIPrivacyOptionDenyAll:
1128                                 privacyType = PURPLE_PRIVACY_DENY_ALL;
1129                                 break;
1130                         case AIPrivacyOptionAllowUsers:
1131                                 privacyType = PURPLE_PRIVACY_ALLOW_USERS;
1132                                 break;
1133                         case AIPrivacyOptionDenyUsers:
1134                                 privacyType = PURPLE_PRIVACY_DENY_USERS;
1135                                 break;
1136                         case AIPrivacyOptionAllowContactList:
1137                                 privacyType = PURPLE_PRIVACY_ALLOW_BUDDYLIST;
1138                                 break;
1139                         
1140                 }
1141                 account->perm_deny = privacyType;
1142                 serv_set_permit_deny(purple_account_get_connection(account));
1143                 AILog(@"Set privacy options for %@ (%x %x) to %i",
1144                           self,account,purple_account_get_connection(account),account->perm_deny);
1145         } else {
1146                 AILog(@"Couldn't set privacy options for %@ (%x %x)",self,account,purple_account_get_connection(account));
1147         }
1150 - (AIPrivacyOption)privacyOptions
1152         AIPrivacyOption privacyOption = -1;
1153         
1154         if (account) {
1155                 PurplePrivacyType privacyType = account->perm_deny;
1156                 
1157                 switch (privacyType) {
1158                         case PURPLE_PRIVACY_ALLOW_ALL:
1159                         default:
1160                                 privacyOption = AIPrivacyOptionAllowAll;
1161                                 break;
1162                         case PURPLE_PRIVACY_DENY_ALL:
1163                                 privacyOption = AIPrivacyOptionDenyAll;
1164                                 break;
1165                         case PURPLE_PRIVACY_ALLOW_USERS:
1166                                 privacyOption = AIPrivacyOptionAllowUsers;
1167                                 break;
1168                         case PURPLE_PRIVACY_DENY_USERS:
1169                                 privacyOption = AIPrivacyOptionDenyUsers;
1170                                 break;
1171                         case PURPLE_PRIVACY_ALLOW_BUDDYLIST:
1172                                 privacyOption = AIPrivacyOptionAllowContactList;
1173                                 break;
1174                 }
1175         }
1176         AILog(@"%@: privacyOptions are %i",self,privacyOption);
1177         return privacyOption;
1180 /*****************************************************/
1181 /* File transfer / AIAccount_Files inherited methods */
1182 /*****************************************************/
1183 #pragma mark File Transfer
1184 - (BOOL)canSendFolders
1186         return NO;
1189 //Create a protocol-specific xfer object, set it up as requested, and begin sending
1190 - (void)_beginSendOfFileTransfer:(ESFileTransfer *)fileTransfer
1192         PurpleXfer *xfer = [self newOutgoingXferForFileTransfer:fileTransfer];
1193         
1194         if (xfer) {
1195                 //Associate the fileTransfer and the xfer with each other
1196                 [fileTransfer setAccountData:[NSValue valueWithPointer:xfer]];
1197                 xfer->ui_data = [fileTransfer retain];
1198                 
1199                 //Set the filename
1200                 purple_xfer_set_local_filename(xfer, [[fileTransfer localFilename] UTF8String]);
1201                 purple_xfer_set_filename(xfer, [[[fileTransfer localFilename] lastPathComponent] UTF8String]);
1202                 
1203                 /*
1204                  Request that the transfer begins.
1205                  We will be asked to accept it via:
1206                         - (void)acceptFileTransferRequest:(ESFileTransfer *)fileTransfer
1207                  below.
1208                  */
1209                 [purpleThread xferRequest:xfer];
1210                 [fileTransfer setStatus: Waiting_on_Remote_User_FileTransfer];
1211         }
1213 //By default, protocols can not create PurpleXfer objects
1214 - (PurpleXfer *)newOutgoingXferForFileTransfer:(ESFileTransfer *)fileTransfer
1216         PurpleXfer                              *newPurpleXfer = NULL;
1218         if (account && purple_account_get_connection(account)) {
1219                 PurplePlugin                            *prpl;
1220                 PurplePluginProtocolInfo  *prpl_info = ((prpl = purple_find_prpl(account->protocol_id)) ?
1221                                                                                           PURPLE_PLUGIN_PROTOCOL_INFO(prpl) :
1222                                                                                           NULL);
1224                 if (prpl_info && prpl_info->new_xfer) {
1225                         char *destsn = (char *)[[[fileTransfer contact] UID] UTF8String];
1226                         newPurpleXfer = (prpl_info->new_xfer)(purple_account_get_connection(account), destsn);
1227                 }
1228         }
1230         return newPurpleXfer;
1233 /* 
1234  * @brief The account requested that we received a file.
1236  * Set up the ESFileTransfer and query the fileTransferController for a save location.
1237  * 
1238  */
1239 - (void)requestReceiveOfFileTransfer:(ESFileTransfer *)fileTransfer
1241         AILog(@"File transfer request received: %@",fileTransfer);
1242         [[adium fileTransferController] receiveRequestForFileTransfer:fileTransfer];
1245 //Create an ESFileTransfer object from an xfer
1246 - (ESFileTransfer *)newFileTransferObjectWith:(NSString *)destinationUID
1247                                                                                  size:(unsigned long long)inSize
1248                                                            remoteFilename:(NSString *)remoteFilename
1250         AIListContact   *contact = [self contactWithUID:destinationUID];
1251     ESFileTransfer      *fileTransfer;
1252         
1253         fileTransfer = [[adium fileTransferController] newFileTransferWithContact:contact
1254                                                                                                                                    forAccount:self
1255                                                                                                                                                  type:Unknown_FileTransfer]; 
1256         [fileTransfer setSize:inSize];
1257         [fileTransfer setRemoteFilename:remoteFilename];
1258         
1259     return fileTransfer;
1262 //Update an ESFileTransfer object progress
1263 - (void)updateProgressForFileTransfer:(ESFileTransfer *)fileTransfer percent:(NSNumber *)percent bytesSent:(NSNumber *)bytesSent
1265         float percentDone = [percent floatValue];
1266     [fileTransfer setPercentDone:percentDone bytesSent:[bytesSent unsignedLongValue]];
1269 //The local side cancelled the transfer.  We probably already have this status set, but set it just in case.
1270 - (void)fileTransferCancelledLocally:(ESFileTransfer *)fileTransfer
1272         if (![fileTransfer isStopped]) {
1273                 [fileTransfer setStatus:Cancelled_Local_FileTransfer];
1274         }
1277 //The remote side cancelled the transfer, the fool. Update our status.
1278 - (void)fileTransferCancelledRemotely:(ESFileTransfer *)fileTransfer
1280         if (![fileTransfer isStopped]) {
1281                 [fileTransfer setStatus:Cancelled_Remote_FileTransfer];
1282         }
1285 - (void)destroyFileTransfer:(ESFileTransfer *)fileTransfer
1287         AILog(@"Destroy file transfer %@",fileTransfer);
1288         [fileTransfer release];
1291 //Accept a send or receive ESFileTransfer object, beginning the transfer.
1292 //Subsequently inform the fileTransferController that the fun has begun.
1293 - (void)acceptFileTransferRequest:(ESFileTransfer *)fileTransfer
1295     AILog(@"Accepted file transfer %@",fileTransfer);
1296         
1297         PurpleXfer              *xfer;
1298         PurpleXferType  xferType;
1299         
1300         xfer = [[fileTransfer accountData] pointerValue];
1302     xferType = purple_xfer_get_type(xfer);
1303     if ( xferType == PURPLE_XFER_SEND ) {
1304         [fileTransfer setFileTransferType:Outgoing_FileTransfer];   
1305     } else if ( xferType == PURPLE_XFER_RECEIVE ) {
1306         [fileTransfer setFileTransferType:Incoming_FileTransfer];
1307                 [fileTransfer setSize:(xfer->size)];
1308     }
1309     
1310     //accept the request
1311         [purpleThread xferRequestAccepted:xfer withFileName:[fileTransfer localFilename]];
1312     
1313         //set the size - must be done after request is accepted?
1315         
1316         [fileTransfer setStatus:Accepted_FileTransfer];
1319 //User refused a receive request.  Tell purple; we don't release the ESFileTransfer object
1320 //since that will happen when the xfer is destroyed.  This will end up calling back on
1321 //- (void)fileTransfercancelledLocally:(ESFileTransfer *)fileTransfer
1322 - (void)rejectFileReceiveRequest:(ESFileTransfer *)fileTransfer
1324         PurpleXfer      *xfer = [[fileTransfer accountData] pointerValue];
1325         if (xfer) {
1326                 [purpleThread xferRequestRejected:xfer];
1327         }
1330 //Cancel a file transfer in progress.  Tell purple; we don't release the ESFileTransfer object
1331 //since that will happen when the xfer is destroyed.  This will end up calling back on
1332 //- (void)fileTransfercancelledLocally:(ESFileTransfer *)fileTransfer
1333 - (void)cancelFileTransfer:(ESFileTransfer *)fileTransfer
1335         PurpleXfer      *xfer = [[fileTransfer accountData] pointerValue];
1336         if (xfer) {
1337                 [purpleThread xferCancel:xfer];
1338         }       
1341 //Account Connectivity -------------------------------------------------------------------------------------------------
1342 #pragma mark Connect
1343 //Connect this account (Our password should be in the instance variable 'password' all ready for us)
1344 - (void)connect
1346         [super connect];
1347         
1348         if (!account) {
1349                 //create a purple account if one does not already exist
1350                 [self createNewPurpleAccount];
1351                 AILog(@"Created PurpleAccount 0x%x with UID %@ and protocolPlugin %s", account, [self UID], [self protocolPlugin]);
1352         }
1353         
1354         //Make sure our settings are correct
1355         if ([self connectivityBasedOnNetworkReachability] &&
1356                 ![[self host] length]) {
1357                 //If we use the network for connectivity, and we don't have a host, we need to get ourselves one. Prompt for it!
1358                 [self promptForHostBeforeConnecting];
1359         } else {
1360                 [self configurePurpleAccountNotifyingTarget:self selector:@selector(continueConnectWithConfiguredPurpleAccount)];
1361         }
1364 static void prompt_host_cancel_cb(CBPurpleAccount *self) {
1365         [self disconnect];
1369 static void prompt_host_ok_cb(CBPurpleAccount *self, const char *host) {
1370         if(host && *host) {
1371                 [self setPreference:[NSString stringWithUTF8String:host]
1372                                          forKey:KEY_CONNECT_HOST
1373                                           group:GROUP_ACCOUNT_STATUS];  
1375                 [self configurePurpleAccountNotifyingTarget:self selector:@selector(continueConnectWithConfiguredPurpleAccount)];
1376         } else {
1377                 prompt_host_cancel_cb(self);
1378         }
1381 - (void)promptForHostBeforeConnecting
1383         purple_request_input(NULL, [[NSString stringWithFormat:AILocalizedString(@"%@ (%@) Setup", "first %@ is an account name; second is a service. This is a title for a window"),
1384                                                                 [self formattedUID], [[self service] shortDescription]] UTF8String],
1385                                                  [AILocalizedString(@"No Server Specified", nil) UTF8String],
1386                                                  [[NSString stringWithFormat:AILocalizedString(@"No server has been configured for the %@ account %@. Please enter one below to connect", nil),
1387                                                    [[self service] longDescription], [self formattedUID]] UTF8String],
1388                                                  /* default value */ "", /* multiline */ FALSE, /* masked */ FALSE, /* hint */ NULL,
1389                                                  [AILocalizedString(@"Connect", "Button title to connect; this is a verb") UTF8String], G_CALLBACK(prompt_host_ok_cb),
1390                                                  [AILocalizedString(@"Cancel", nil) UTF8String], G_CALLBACK(prompt_host_cancel_cb),
1391                                                  /* account */ NULL, /* who */ NULL, /* conv */ NULL,
1392                                                  self);
1393                                                  
1397 - (void)continueConnectWithConfiguredPurpleAccount
1399         //Configure libpurple's proxy settings; continueConnectWithConfiguredProxy will be called once we are ready
1400         [self configureAccountProxyNotifyingTarget:self selector:@selector(continueConnectWithConfiguredProxy)];
1403 - (void)continueConnectWithConfiguredProxy
1405         //Set password and connect
1406         purple_account_set_password(account, [password UTF8String]);
1408         //Set our current status state after filtering its statusMessage as appropriate. This will take us online in the process.
1409         AIStatus        *statusState = [self statusObjectForKey:@"StatusState"];
1410         if (!statusState || ([statusState statusType] == AIOfflineStatusType)) {
1411                 statusState = [[adium statusController] defaultInitialStatusState];
1412         }
1414         AILog(@"Adium: Connect: %@ initiating connection using status state %@ (%@).",[self UID],statusState,
1415                           [statusState statusMessageString]);
1417         [self autoRefreshingOutgoingContentForStatusKey:@"StatusState"
1418                                                                                    selector:@selector(gotFilteredStatusMessage:forStatusState:)
1419                                                                                         context:statusState];
1423 //Make sure our settings are correct; notify target/selector when we're finished
1424 - (void)configurePurpleAccountNotifyingTarget:(id)target selector:(SEL)selector
1426         NSInvocation    *contextInvocation;
1427         
1428         //Perform the synchronous configuration activities (subclasses may want to take action in this function)
1429         [self configurePurpleAccount];
1430         
1431         contextInvocation = [NSInvocation invocationWithMethodSignature:[target methodSignatureForSelector:selector]];
1432         
1433         [contextInvocation setTarget:target];
1434         [contextInvocation setSelector:selector];
1435         [contextInvocation retainArguments];
1437         //Set the text profile BEFORE beginning the connect process, to avoid problems with setting it while the
1438         //connect occurs. Once that's done, contextInvocation will be invoked, continuing the configurePurpleAccount process.
1439         [self autoRefreshingOutgoingContentForStatusKey:@"TextProfile" 
1440                                                                                    selector:@selector(setAccountProfileTo:configurePurpleAccountContext:)
1441                                                                                         context:contextInvocation];
1445  * @brief The server name to be passed to libpurple
1446  * By default, this is the host as seen by the rest of Adium.  Subclasses may choose to override this if
1447  * some trickery is desired between what is told to libpurple and what the rest of Adium sees.
1448  */
1449 - (NSString *)hostForPurple
1451         return [self host];
1454 //Synchronous purple account configuration activites, always performed after an account is created.
1455 //This is a definite subclassing point so prpls can apply their own account settings.
1456 - (void)configurePurpleAccount
1458         NSString        *hostName;
1459         int                     portNumber;
1461         //Host (server)
1462         hostName = [self hostForPurple];
1463         if (hostName && [hostName length]) {
1464                 purple_account_set_string(account, "server", [hostName UTF8String]);
1465         }
1466         
1467         //Port
1468         portNumber = [self port];
1469         if (portNumber) {
1470                 purple_account_set_int(account, "port", portNumber);
1471         }
1472         
1473         //E-mail checking
1474         purple_account_set_check_mail(account, [[self shouldCheckMail] boolValue]);
1475         
1476         //Update a few status keys before we begin connecting.  Libpurple will send these automatically
1477     [self updateStatusForKey:KEY_USER_ICON];
1481  * @brief Configure libpurple's proxy settings using the current system values
1483  * target/selector are used rather than a hardcoded callback (or getProxyConfigurationNotifyingTarget: directly) because this allows code reuse
1484  * between the connect and register processes, which are similar in their need for proxy configuration
1485  */
1486 - (void)configureAccountProxyNotifyingTarget:(id)target selector:(SEL)selector
1488         NSInvocation            *invocation; 
1490         //Configure the invocation we will use when we are done configuring
1491         invocation = [NSInvocation invocationWithMethodSignature:[target methodSignatureForSelector:selector]];
1492         [invocation setSelector:selector];
1493         [invocation setTarget:target];
1494         
1495         [self getProxyConfigurationNotifyingTarget:self
1496                                                                           selector:@selector(retrievedProxyConfiguration:context:)
1497                                                                            context:invocation];
1501  * @brief Callback for -[self getProxyConfigurationNotifyingTarget:selector:context:]
1502  */
1503 - (void)retrievedProxyConfiguration:(NSDictionary *)proxyConfig context:(NSInvocation *)invocation
1505         PurpleProxyInfo         *proxy_info;
1506         
1507         AdiumProxyType          proxyType = [[proxyConfig objectForKey:@"AdiumProxyType"] intValue];
1508         
1509         proxy_info = purple_proxy_info_new();
1510         purple_account_set_proxy_info(account, proxy_info);
1512         PurpleProxyType         purpleAccountProxyType;
1513         
1514         switch (proxyType) {
1515                 case Adium_Proxy_HTTP:
1516                 case Adium_Proxy_Default_HTTP:
1517                         purpleAccountProxyType = PURPLE_PROXY_HTTP;
1518                         break;
1519                 case Adium_Proxy_SOCKS4:
1520                 case Adium_Proxy_Default_SOCKS4:
1521                         purpleAccountProxyType = PURPLE_PROXY_SOCKS4;
1522                         break;
1523                 case Adium_Proxy_SOCKS5:
1524                 case Adium_Proxy_Default_SOCKS5:
1525                         purpleAccountProxyType = PURPLE_PROXY_SOCKS5;
1526                         break;
1527                 case Adium_Proxy_None:
1528                 default:
1529                         purpleAccountProxyType = PURPLE_PROXY_NONE;
1530                         break;
1531         }
1532         
1533         purple_proxy_info_set_type(proxy_info, purpleAccountProxyType);
1535         if (proxyType != Adium_Proxy_None) {
1536                 purple_proxy_info_set_host(proxy_info, (char *)[[proxyConfig objectForKey:@"Host"] UTF8String]);
1537                 purple_proxy_info_set_port(proxy_info, [[proxyConfig objectForKey:@"Port"] intValue]);
1539                 purple_proxy_info_set_username(proxy_info, (char *)[[proxyConfig objectForKey:@"Username"] UTF8String]);
1540                 purple_proxy_info_set_password(proxy_info, (char *)[[proxyConfig objectForKey:@"Password"] UTF8String]);
1541                 
1542                 AILog(@"Connecting with proxy type %i and proxy host %@",proxyType, [proxyConfig objectForKey:@"Host"]);
1543         }
1545         [invocation invoke];
1548 //Sublcasses should override to provide a string for each progress step
1549 - (NSString *)connectionStringForStep:(int)step { return nil; };
1552  * @brief Should the account's status be updated as soon as it is connected?
1554  * If YES, the StatusState and IdleSince status keys will be told to update as soon as the account connects.
1555  * This will allow the account to send its status information to the server upon connecting.
1557  * If this information is already known by the account at the time it connects and further prompting to send it is
1558  * not desired, return NO.
1560  * libpurple should already have been told of our status before connecting began.
1561  */
1562 - (BOOL)updateStatusImmediatelyAfterConnecting
1564         return NO;
1567 //Our account has connected
1568 - (void)accountConnectionConnected
1570         AILog(@"************ %@ CONNECTED ***********",[self UID]);
1571         
1572         [self didConnect];
1574         [[adium notificationCenter] addObserver:self
1575                                                                    selector:@selector(iTunesDidUpdate:)
1576                                                                            name:Adium_iTunesTrackChangedNotification
1577                                                                          object:nil];
1578         
1579     //Silence updates
1580     [self silenceAllContactUpdatesForInterval:18.0];
1581         [[adium contactController] delayListObjectNotificationsUntilInactivity];
1582         
1583         //Clear any previous disconnection error
1584         [self setLastDisconnectionError:nil];
1585         
1586         if(deletionDialog)
1587                 [purpleThread unregisterAccount:self];
1590 - (void)accountConnectionProgressStep:(NSNumber *)step percentDone:(NSNumber *)connectionProgressPrecent
1592         NSString        *connectionProgressString = [self connectionStringForStep:[step intValue]];
1594         [self setStatusObject:connectionProgressString forKey:@"ConnectionProgressString" notify:NO];
1595         [self setStatusObject:connectionProgressPrecent forKey:@"ConnectionProgressPercent" notify:NO]; 
1597         //Apply any changes
1598         [self notifyOfChangedStatusSilently:NO];
1599         
1600         AILog(@"************ %@ --step-- %i",[self UID],[step intValue]);
1604  * @brief Name to use when creating a PurpleAccount for this CBPurpleAccount
1606  * By default, we just use the formattedUID.  Subclasses can override this to provide other handling,
1607  * such as appending @mac.com if necessary for dotMac accounts.
1608  */
1609 - (const char *)purpleAccountName
1611         return [[self formattedUID] UTF8String];
1614 - (void)createNewPurpleAccount
1616         if (!purpleThread) {
1617                 purpleThread = [[SLPurpleCocoaAdapter sharedInstance] retain];  
1618         }       
1620         //Create a fresh version of the account
1621     if ((account = purple_account_new([self purpleAccountName], [self protocolPlugin]))) {
1622                 [purpleThread addAdiumAccount:self];
1623         } else {
1624                 AILog(@"Unable to create Libpurple account with name %s and protocol plugin %s",
1625                           [self purpleAccountName], [self protocolPlugin]);
1626                 NSLog(@"Unable to create Libpurple account with name %s and protocol plugin %s",
1627                           [self purpleAccountName], [self protocolPlugin]);
1628         }
1631 #pragma mark Disconnect
1634  * @brief Disconnect this account
1635  */
1636 - (void)disconnect
1638         if ([self online] || [self integerStatusObjectForKey:@"Connecting"]) {
1639                 //As per AIAccount's documentation, call super's implementation
1640                 [super disconnect];
1642                 [[adium contactController] delayListObjectNotificationsUntilInactivity];
1644                 //Tell libpurple to disconnect
1645                 [purpleThread disconnectAccount:self];
1646         }
1649 - (void)setLastDisconnectionReason:(PurpleConnectionError)reason
1651         lastDisconnectionReason = reason;
1654 - (PurpleConnectionError)lastDisconnectionReason
1656         return lastDisconnectionReason;
1660  * @brief Our account was unexpectedly disconnected with an error message
1661  */
1662 - (void)accountConnectionReportDisconnect:(NSString *)text withReason:(PurpleConnectionError)reason
1664         [self setLastDisconnectionError:text];
1665         [self setLastDisconnectionReason:reason];
1667         if (reason == PURPLE_CONNECTION_ERROR_AUTHENTICATION_FAILED)
1668                 [self serverReportedInvalidPassword];
1670         //We are disconnecting
1671     [self setStatusObject:[NSNumber numberWithBool:YES] forKey:@"Disconnecting" notify:NotifyNow];
1672         
1673         AILog(@"%@ accountConnectionReportDisconnect: %@",self,lastDisconnectionError);
1676 - (void)accountConnectionNotice:(NSString *)connectionNotice
1678     [[adium interfaceController] handleErrorMessage:[NSString stringWithFormat:AILocalizedString(@"%@ (%@) : Connection Notice",nil),[self formattedUID],[service description]]
1679                                     withDescription:connectionNotice];
1684  * @brief Our account has disconnected
1686  * This is called after the account disconnects for any reason
1687  */
1688 - (void)accountConnectionDisconnected
1690         //Clear status objects which don't make sense for a disconnected account
1691         [self setStatusObject:nil forKey:@"TextProfile" notify:NO];
1693         //Apply any changes
1694         [self notifyOfChangedStatusSilently:NO];
1695         
1696         [[adium notificationCenter] removeObserver:self
1697                                                                                   name:Adium_iTunesTrackChangedNotification
1698                                                                                 object:nil];
1699         [tuneinfo release];
1700         tuneinfo = nil;
1701         //Report that we disconnected
1702         AILog(@"%@: Telling the core we disconnected", self);
1703         [self didDisconnect];
1705         if (willBeDeleted)
1706                 [super alertForAccountDeletion:deletionDialog didReturn:NSAlertDefaultReturn];
1709 - (AIReconnectDelayType)shouldAttemptReconnectAfterDisconnectionError:(NSString **)disconnectionError
1711         AIReconnectDelayType reconnectDelayType;
1713         if ([self lastDisconnectionReason] == PURPLE_CONNECTION_ERROR_AUTHENTICATION_FAILED) {
1714                 [self setLastDisconnectionError:AILocalizedString(@"Incorrect username or password","Error message displayed when the server reports username or password as being incorrect.")];
1715                 reconnectDelayType = AIReconnectImmediately;
1717         } else if ([self lastDisconnectionReason] == PURPLE_CONNECTION_ERROR_INVALID_USERNAME) {
1718                 reconnectDelayType = AIReconnectNever;
1719                 //Enable this after Adium 1.2, which is in string freeze as it is added.
1720                 /* *disconnectionError = AILocalizedString(@"The name you entered is not registered. Check to ensure you typed it correctly.", nil); */
1722         } else if (purple_connection_error_is_fatal([self lastDisconnectionReason])) {
1723                 reconnectDelayType = AIReconnectNever;
1725         } else {
1726                 reconnectDelayType = AIReconnectNormally;
1727         }
1729         return reconnectDelayType;
1732 #pragma mark Registering
1733 - (void)performRegisterWithPassword:(NSString *)inPassword
1735         //Save the new password
1736         if (password != inPassword) {
1737                 [password release]; password = [inPassword retain];
1738         }
1739         
1740         if (!account) {
1741                 //create a purple account if one does not already exist
1742                 [self createNewPurpleAccount];
1743                 AILog(@"Registering: created PurpleAccount 0x%x with UID %@, protocolPlugin %s", account, [self UID], [self protocolPlugin]);
1744         }
1745         
1746         //We are connecting
1747         [self setStatusObject:[NSNumber numberWithBool:YES] forKey:@"Connecting" notify:NotifyNow];
1748         
1749         //Make sure our settings are correct
1750         [self configurePurpleAccountNotifyingTarget:self selector:@selector(continueRegisterWithConfiguredPurpleAccount)];
1753 - (void)continueRegisterWithConfiguredProxy
1755         //Set password and connect
1756         purple_account_set_password(account, [password UTF8String]);
1757         
1758         AILog(@"Adium: Register: %@ initiating connection.",[self UID]);
1759         
1760         [purpleThread registerAccount:self];
1763 - (void)continueRegisterWithConfiguredPurpleAccount
1765         //Configure libpurple's proxy settings; continueConnectWithConfiguredProxy will be called once we are ready
1766         [self configureAccountProxyNotifyingTarget:self selector:@selector(continueRegisterWithConfiguredProxy)];
1769 - (void)purpleAccountRegistered:(BOOL)success
1771         if (success && [[self service] accountViewController]) {
1772                 NSString *username = (account->username ? [NSString stringWithUTF8String:account->username] : [NSNull null]);
1773                 NSString *pw = (account->password ? [NSString stringWithUTF8String:account->password] : [NSNull null]);
1775                 [[adium notificationCenter] postNotificationName:AIAccountUsernameAndPasswordRegisteredNotification
1776                                                                                                   object:self
1777                                                                                                 userInfo:[NSDictionary dictionaryWithObjectsAndKeys:
1778                                                                                                         username, @"username",
1779                                                                                                         pw, @"password",
1780                                                                                                         nil]];
1781         }
1784 //Account Status ------------------------------------------------------------------------------------------------------
1785 #pragma mark Account Status
1786 //Status keys this account supports
1787 - (NSSet *)supportedPropertyKeys
1789         static NSMutableSet *supportedPropertyKeys = nil;
1790         
1791         if (!supportedPropertyKeys) {
1792                 supportedPropertyKeys = [[NSMutableSet alloc] initWithObjects:
1793                         @"IdleSince",
1794                         @"IdleManuallySet",
1795                         @"TextProfile",
1796                         @"DefaultUserIconFilename",
1797                         KEY_ACCOUNT_CHECK_MAIL,
1798                         nil];
1799                 [supportedPropertyKeys unionSet:[super supportedPropertyKeys]];
1800                 
1801         }
1803         return supportedPropertyKeys;
1806 //Update our status
1807 - (void)updateStatusForKey:(NSString *)key
1808 {    
1809         [super updateStatusForKey:key];
1810         
1811     //Now look at keys which only make sense if we have an account
1812         if (account) {
1813                 AILog(@"%@: Updating status for key: %@",self, key);
1815                 if ([key isEqualToString:@"IdleSince"]) {
1816                         NSDate  *idleSince = [self preferenceForKey:@"IdleSince" group:GROUP_ACCOUNT_STATUS];
1817                         [self setAccountIdleSinceTo:idleSince];
1818                                                         
1819                 } else if ([key isEqualToString:@"TextProfile"]) {
1820                         [self autoRefreshingOutgoingContentForStatusKey:key selector:@selector(setAccountProfileTo:)];
1821                         
1822                 } else if ([key isEqualToString:KEY_USER_ICON]) {
1823                         NSData  *data = [self userIconData];
1825                         [self setAccountUserImageData:data];
1827                 } else if ([key isEqualToString:KEY_ACCOUNT_CHECK_MAIL]) {
1828                         //Update the mail checking setting if the account is already made (if it isn't, we'll set it when it is made)
1829                         if (account) {
1830                                 [purpleThread setCheckMail:[self shouldCheckMail]
1831                                                           forAccount:self];
1832                         }
1833                 }
1834         }
1838  * @brief Return the purple status type to be used for a status
1840  * Most subclasses should override this method; these generic values may be appropriate for others.
1842  * Active services provided nonlocalized status names.  An AIStatus is passed to this method along with a pointer
1843  * to the status message.  This method should handle any status whose statusNname this service set as well as any statusName
1844  * defined in  AIStatusController.h (which will correspond to the services handled by Adium by default).
1845  * It should also handle a status name not specified in either of these places with a sane default, most likely by loooking at
1846  * [statusState statusType] for a general idea of the status's type.
1848  * @param statusState The status for which to find the purple status ID
1849  * @param arguments Prpl-specific arguments which will be passed with the state. Message is handled automatically.
1851  * @result The purple status ID
1852  */
1853 - (const char *)purpleStatusIDForStatus:(AIStatus *)statusState
1854                                                         arguments:(NSMutableDictionary *)arguments
1856         char    *statusID = NULL;
1857         
1858         switch ([statusState statusType]) {
1859                 case AIAvailableStatusType:
1860                         statusID = "available";
1861                         break;
1862                 case AIAwayStatusType:
1863                         statusID = "away";
1864                         break;
1865                         
1866                 case AIInvisibleStatusType:
1867                         statusID = "invisible";
1868                         break;
1869                         
1870                 case AIOfflineStatusType:
1871                         statusID = "offline";
1872                         break;
1873         }
1874         
1875         return statusID;
1878 - (BOOL)shouldAddMusicalNoteToNowPlayingStatus
1880         return YES;
1883 - (BOOL)shouldSetITMSLinkForNowPlayingStatus
1885         return NO;
1888 - (BOOL)shouldIncludeNowPlayingInformationInAllStatuses
1890         return NO;
1893 - (void)iTunesDidUpdate:(NSNotification*)notification {
1894         if ([self shouldIncludeNowPlayingInformationInAllStatuses]) {
1895                 [tuneinfo release];
1896                 tuneinfo = [[notification object] retain];
1897                 
1898                 // update info in prpl
1899                 if ([self online])
1900                         [self updateStatusForKey:@"StatusState"];
1901         }
1905  * @brief Perform the setting of a status state
1907  * Sets the account to a passed status state.  The account should set itself to best possible status given the return
1908  * values of statusState's accessors.  The passed statusMessage has been filtered; it should be used rather than
1909  * [statusState statusMessage], which returns an unfiltered statusMessage.
1911  * @param statusState The state to enter
1912  * @param statusMessage The filtered status message to use.
1913  */
1914 - (void)setStatusState:(AIStatus *)statusState usingStatusMessage:(NSAttributedString *)statusMessage
1916         NSString                        *encodedStatusMessage;
1917         NSMutableDictionary     *arguments = [[NSMutableDictionary alloc] init];
1919         //Get the purple status type from this class or subclasses, which may also potentially modify or nullify our statusMessage
1920         const char *statusID = [self purpleStatusIDForStatus:statusState
1921                                                                                          arguments:arguments];
1923         if (![statusMessage length] &&
1924                 ([statusState statusType] == AIAwayStatusType) &&
1925                 [statusState statusName] &&
1926                 (!statusID || (strcmp(statusID, "away") == 0))) {
1927                 /* If we don't have a status message, and the status type is away for a non-default away such as "Do Not Disturb", and we're only setting
1928                  * a default away state becuse we don't know a better one for this service, get a default
1929                  * description of this away state. This allows, for example, an AIM user to set the "Do Not Disturb" type provided by her ICQ account
1930                  * and have the away message be set appropriately.
1931                  */
1932                 statusMessage = [NSAttributedString stringWithString:[[adium statusController] descriptionForStateOfStatus:statusState]];
1933         }
1935         BOOL isNowPlayingStatus = ([statusState specialStatusType] == AINowPlayingSpecialStatusType);
1936         if (isNowPlayingStatus && [statusMessage length]) {
1937                 if ([self shouldAddMusicalNoteToNowPlayingStatus]) {
1938 #define MUSICAL_NOTE_AND_SPACE [NSString stringWithUTF8String:"\xe2\x99\xab "]
1939                         NSMutableAttributedString *temporaryStatusMessage;
1940                         temporaryStatusMessage = [[[NSMutableAttributedString alloc] initWithString:MUSICAL_NOTE_AND_SPACE] autorelease];
1941                         [temporaryStatusMessage appendAttributedString:statusMessage];
1942                         
1943                         statusMessage = temporaryStatusMessage;
1944                 }
1945                 
1946                 if ([self shouldSetITMSLinkForNowPlayingStatus]) {
1947                         //Grab the message's subtext, which is the song link if we're using the Current iTunes Track status
1948                         NSString *itmsStoreLink = [statusMessage attribute:@"AIMessageSubtext" atIndex:0 effectiveRange:NULL];
1949                         if (itmsStoreLink) {
1950                                 [arguments setObject:itmsStoreLink
1951                                                           forKey:@"itmsurl"];
1952                         }
1953                 }
1954         }
1956         if (isNowPlayingStatus || [self shouldIncludeNowPlayingInformationInAllStatuses]) {
1957                 if (tuneinfo && [[tuneinfo objectForKey:ITUNES_PLAYER_STATE] isEqualToString:@"Playing"]) {
1958                         NSString *artist = [tuneinfo objectForKey:ITUNES_ARTIST];
1959                         NSString *name = [tuneinfo objectForKey:ITUNES_NAME];
1961                         [arguments setObject:(artist ? artist : @"") forKey:[NSString stringWithUTF8String:PURPLE_TUNE_ARTIST]];
1962                         [arguments setObject:(name ? name : @"") forKey:[NSString stringWithUTF8String:PURPLE_TUNE_TITLE]];
1963                         [arguments setObject:([tuneinfo objectForKey:ITUNES_ALBUM] ? [tuneinfo objectForKey:ITUNES_ALBUM] : @"") forKey:[NSString stringWithUTF8String:PURPLE_TUNE_ALBUM]];
1964                         [arguments setObject:([tuneinfo objectForKey:ITUNES_GENRE] ? [tuneinfo objectForKey:ITUNES_GENRE] : @"") forKey:[NSString stringWithUTF8String:PURPLE_TUNE_GENRE]];
1965                         [arguments setObject:([tuneinfo objectForKey:ITUNES_TOTAL_TIME] ? [tuneinfo objectForKey:ITUNES_TOTAL_TIME]:[NSNumber numberWithInt:-1]) forKey:[NSString stringWithUTF8String:PURPLE_TUNE_TIME]];
1966                         [arguments setObject:([tuneinfo objectForKey:ITUNES_YEAR] ? [tuneinfo objectForKey:ITUNES_YEAR]:[NSNumber numberWithInt:-1]) forKey:[NSString stringWithUTF8String:PURPLE_TUNE_YEAR]];
1967                         [arguments setObject:([tuneinfo objectForKey:ITUNES_STORE_URL] ? [tuneinfo objectForKey:ITUNES_STORE_URL] : @"") forKey:[NSString stringWithUTF8String:PURPLE_TUNE_URL]];
1968                         
1969                         [arguments setObject:[NSString stringWithFormat:@"%@%@%@", (name ? name : @""), (name && artist ? @" - " : @""), (artist ? artist : @"")]
1970                                                   forKey:[NSString stringWithUTF8String:PURPLE_TUNE_FULL]];
1971                         
1972                 } else {
1973                         [arguments setObject:@"" forKey:[NSString stringWithUTF8String:PURPLE_TUNE_ARTIST]];
1974                         [arguments setObject:@"" forKey:[NSString stringWithUTF8String:PURPLE_TUNE_TITLE]];
1975                         [arguments setObject:@"" forKey:[NSString stringWithUTF8String:PURPLE_TUNE_ALBUM]];
1976                         [arguments setObject:@"" forKey:[NSString stringWithUTF8String:PURPLE_TUNE_GENRE]];
1977                         [arguments setObject:[NSNumber numberWithInt:-1] forKey:[NSString stringWithUTF8String:PURPLE_TUNE_TIME]];
1978                         [arguments setObject:[NSNumber numberWithInt:-1] forKey:[NSString stringWithUTF8String:PURPLE_TUNE_YEAR]];
1979                         [arguments setObject:@"" forKey:[NSString stringWithUTF8String:PURPLE_TUNE_URL]];
1980                         [arguments setObject:@"" forKey:[NSString stringWithUTF8String:PURPLE_TUNE_FULL]];
1981                 }
1982         }
1984         //Encode the status message if we have one
1985         encodedStatusMessage = (statusMessage ? 
1986                                                         [self encodedAttributedString:statusMessage
1987                                                                                    forStatusState:statusState]  :
1988                                                         nil);
1989         if (encodedStatusMessage) {
1990                 [arguments setObject:encodedStatusMessage
1991                                           forKey:@"message"];
1992         }
1994         [self setStatusState:statusState
1995                                 statusID:statusID
1996                                 isActive:[NSNumber numberWithBool:YES] /* We're only using exclusive states for now... I hope.  */
1997                            arguments:arguments];
1998         
1999         [arguments release];
2003  * @brief Perform the actual setting of a state
2005  * This is called by setStatusState.  It allows subclasses to perform any other behaviors, such as modifying a display
2006  * name, which are called for by the setting of the state; most of the processing has already been done, however, so
2007  * most subclasses will not need to implement this.
2009  * @param statusState The AIStatus which is being set
2010  * @param statusID The Purple-sepcific statusID we are setting
2011  * @param isActive An NSNumber with a bool YES if we are activating (going to) the passed state, NO if we are deactivating (going away from) the passed state.
2012  * @param arguments Purple-specific arguments specified by the account. It must contain only NSString objects and keys.
2013  */
2014 - (void)setStatusState:(AIStatus *)statusState statusID:(const char *)statusID isActive:(NSNumber *)isActive arguments:(NSMutableDictionary *)arguments
2016         [purpleThread setStatusID:statusID
2017                                    isActive:isActive
2018                                   arguments:arguments
2019                                   onAccount:self];
2022 //Set our idle (Pass nil for no idle)
2023 - (void)setAccountIdleSinceTo:(NSDate *)idleSince
2025         [purpleThread setIdleSinceTo:idleSince onAccount:self];
2026         
2027         //We now should update our idle status object
2028         [self setStatusObject:([idleSince timeIntervalSinceNow] ? idleSince : nil)
2029                                    forKey:@"IdleSince"
2030                                    notify:NotifyNow];
2033 //Set the profile, then invoke the passed invocation to return control to the target/selector specified
2034 //by a configurePurpleAccountNotifyingTarget:selector: call.
2035 - (void)setAccountProfileTo:(NSAttributedString *)profile configurePurpleAccountContext:(NSInvocation *)inInvocation
2037         [self setAccountProfileTo:profile];
2038         
2039         [inInvocation invoke];
2042 //Set our profile immediately on the purpleThread
2043 - (void)setAccountProfileTo:(NSAttributedString *)profile
2045         if (!profile || ![[profile string] isEqualToString:[[self statusObjectForKey:@"TextProfile"] string]]) {
2046                 NSString        *profileHTML = nil;
2047                 
2048                 //Convert the profile to HTML, and pass it to libpurple
2049                 if (profile) {
2050                         profileHTML = [self encodedAttributedString:profile forListObject:nil];
2051                 }
2052                 
2053                 [purpleThread setInfo:profileHTML onAccount:self];
2054                 
2055                 //We now have a profile
2056                 [self setStatusObject:profile forKey:@"TextProfile" notify:NotifyNow];
2057         }
2061  * @brief Set our user image
2063  * Pass nil for no image. This resizes and converts the image as needed for our protocol.
2064  * After setting it with purple, it sets it within Adium; if this is not called, the image will
2065  * show up neither locally nor remotely.
2066  */
2067 - (void)setAccountUserImageData:(NSData *)originalData
2069         NSImage *image =  (originalData ? [[[NSImage alloc] initWithData:originalData] autorelease] : nil);
2071         if (account) {
2072                 NSSize          imageSize = [image size];
2073                 
2074                 //Clear the existing icon first
2075                 [purpleThread setBuddyIcon:nil onAccount:self];
2076                 
2077                 /* Now pass libpurple the new icon.  Libpurple takes icons as a file, so we save our
2078                  * image to one, and then pass libpurple the path. Check to be sure our image doesn't have an NSZeroSize size,
2079                  * which would indicate currupt data */
2080                 if (image && !NSEqualSizes(NSZeroSize, imageSize)) {
2081                         PurplePlugin                            *prpl;
2082                         PurplePluginProtocolInfo  *prpl_info = ((prpl = purple_find_prpl(account->protocol_id)) ?
2083                                                                                                   PURPLE_PLUGIN_PROTOCOL_INFO(prpl) :
2084                                                                                                   NULL);
2086                         AILog(@"Original image of size %f %f",imageSize.width,imageSize.height);
2088                         if (prpl_info && (prpl_info->icon_spec.format)) {
2089                                 NSData          *buddyIconData = nil;
2090                                 BOOL            smallEnough, prplScales;
2091                                 unsigned        i;
2092                                 
2093                                 /* We need to scale it down if:
2094                                  *      1) The prpl needs to scale before it sends to the server or other buddies AND
2095                                  *      2) The image is larger than the maximum size allowed by the protocol
2096                                  * We ignore the minimum required size, as scaling up just leads to pixellated images.
2097                                  */
2098                                 smallEnough =  (prpl_info->icon_spec.max_width >= imageSize.width &&
2099                                                                 prpl_info->icon_spec.max_height >= imageSize.height);
2100                                         
2101                                 prplScales = (prpl_info->icon_spec.scale_rules & PURPLE_ICON_SCALE_SEND) || (prpl_info->icon_spec.scale_rules & PURPLE_ICON_SCALE_DISPLAY);
2103                                 if (prplScales && !smallEnough) {
2104                                         int width = imageSize.width;
2105                                         int height = imageSize.height;
2106                                         
2107                                         purple_buddy_icon_get_scale_size(&prpl_info->icon_spec, &width, &height);
2108                                         //Determine the scaled size.  If it's too big, scale to the largest permissable size
2109                                         image = [image imageByScalingToSize:NSMakeSize(width, height)];
2111                                         /* Our original data is no longer valid, since we had to scale to a different size */
2112                                         originalData = nil;
2113                                         AILog(@"%@: Scaled image to size %@", self, NSStringFromSize([image size]));
2114                                 }
2116                                 if (!buddyIconData) {
2117                                         char            **prpl_formats =  g_strsplit(prpl_info->icon_spec.format,",",0);
2119                                         //Look for gif first if the image is animated
2120                                         NSImageRep      *imageRep = [image bestRepresentationForDevice:nil] ;
2121                                         if ([imageRep isKindOfClass:[NSBitmapImageRep class]] &&
2122                                                 [[(NSBitmapImageRep *)imageRep valueForProperty:NSImageFrameCount] intValue] > 1) {
2123                                                 
2124                                                 for (i = 0; prpl_formats[i]; i++) {
2125                                                         if (strcmp(prpl_formats[i],"gif") == 0) {
2126                                                                 /* Try to use our original data.  If we had to scale, originalData will have been set
2127                                                                 * to nil and we'll continue below to convert the image. */
2128                                                                 AILog(@"l33t script kiddie animated GIF!!111");
2129                                                                 
2130                                                                 buddyIconData = originalData;
2131                                                                 if (buddyIconData)
2132                                                                         break;
2133                                                         }
2134                                                 }
2135                                         }
2136                                         
2137                                         if (!buddyIconData) {
2138                                                 for (i = 0; prpl_formats[i]; i++) {
2139                                                         if (strcmp(prpl_formats[i],"png") == 0) {
2140                                                                 buddyIconData = [image PNGRepresentation];
2141                                                                 if (buddyIconData)
2142                                                                         break;
2143                                                                 
2144                                                         } else if ((strcmp(prpl_formats[i],"jpeg") == 0) || (strcmp(prpl_formats[i],"jpg") == 0)) {                                                             
2145                                                                 buddyIconData = [image JPEGRepresentationWithCompressionFactor:1.0];
2146                                                                 if (buddyIconData)
2147                                                                         break;
2148                                                                 
2149                                                         } else if ((strcmp(prpl_formats[i],"tiff") == 0) || (strcmp(prpl_formats[i],"tif") == 0)) {
2150                                                                 buddyIconData = [image TIFFRepresentation];
2151                                                                 if (buddyIconData)
2152                                                                         break;
2153                                                                 
2154                                                         } else if (strcmp(prpl_formats[i],"gif") == 0) {
2155                                                                 buddyIconData = [image GIFRepresentation];
2156                                                                 if (buddyIconData)
2157                                                                         break;
2158                                                                 
2159                                                         } else if (strcmp(prpl_formats[i],"bmp") == 0) {
2160                                                                 buddyIconData = [image BMPRepresentation];
2161                                                                 if (buddyIconData)
2162                                                                         break;
2163                                                                 
2164                                                         }                                               
2165                                                 }
2166                                                 
2167                                                 size_t maxSize = prpl_info->icon_spec.max_filesize;
2168                                                 if (maxSize > 0 && ([buddyIconData length] > maxSize)) {
2169                                                         AILog(@"Image %i is larger than %i!",[buddyIconData length],maxSize);
2170                                                         for (i = 0; prpl_formats[i]; i++) {
2171                                                                 if ((strcmp(prpl_formats[i],"jpeg") == 0) || (strcmp(prpl_formats[i],"jpg") == 0)) {
2172                                                                         float compressionFactor;
2173                                                                         for (compressionFactor = 0.99; compressionFactor > 0.4; compressionFactor -= 0.01) {
2174                                                                                 buddyIconData = [image JPEGRepresentationWithCompressionFactor:compressionFactor];
2175                                                                                 
2176                                                                                 if (buddyIconData && ([buddyIconData length] <= maxSize)) {
2177                                                                                         AILog(@"Succeeded getting it down to %i with compressionFactor %f",[buddyIconData length],compressionFactor);
2178                                                                                         break;
2179                                                                                 }
2180                                                                         }
2181                                                                 }
2182                                                         }
2183                                                 }
2184                                         }       
2185                                         //Cleanup
2186                                         g_strfreev(prpl_formats);
2187                                 }
2189                                 [purpleThread setBuddyIcon:buddyIconData onAccount:self];
2190                         }
2191                 }
2192         }
2193         
2194         //We now have an icon
2195         [self setStatusObject:image forKey:KEY_USER_ICON notify:NotifyNow];
2198 #pragma mark Group Chat
2199 - (BOOL)inviteContact:(AIListContact *)inContact toChat:(AIChat *)inChat withMessage:(NSString *)inviteMessage
2201         [purpleThread inviteContact:inContact toChat:inChat withMessage:inviteMessage];
2202         
2203         return YES;
2206 #pragma mark Buddy Menu Items
2207 //Action of a dynamically-generated contact menu item
2208 - (void)performContactMenuAction:(NSMenuItem *)sender
2210         NSDictionary            *dict = [sender representedObject];
2211         
2212         [purpleThread performContactMenuActionFromDict:dict forAccount:self];
2216  * @brief Utility method when generating buddy-specific menu items
2218  * Adds the menu item for act to a growing array of NSMenuItems.  If act has children (a submenu), this method is used recursively
2219  * to generate the submenu containing each child menu item.
2220  */
2221 - (void)addMenuItemForMenuAction:(PurpleMenuAction *)act forListContact:(AIListContact *)inContact purpleBuddy:(PurpleBuddy *)buddy toArray:(NSMutableArray *)menuItemArray withServiceIcon:(NSImage *)serviceIcon
2223         NSDictionary    *dict;
2224         NSMenuItem              *menuItem;
2225         NSString                *title;
2226                                 
2227         //If titleForContactMenuLabel:forContact: returns nil, we don't add the menuItem
2228         if (act &&
2229                 act->label &&
2230                 (title = [self titleForContactMenuLabel:act->label
2231                                                                          forContact:inContact])) { 
2232                 menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:title
2233                                                                                                                                                 target:self
2234                                                                                                                                                 action:@selector(performContactMenuAction:)
2235                                                                                                                                  keyEquivalent:@""];
2236                 [menuItem setImage:serviceIcon];
2238                 if (act->data) {
2239                         dict = [NSDictionary dictionaryWithObjectsAndKeys:
2240                                 [NSValue valueWithPointer:act->callback],@"PurpleMenuActionCallback",
2241                                 /* act->data may be freed by purple_menu_action_free() before we use it, I'm afraid... */
2242                                 [NSValue valueWithPointer:act->data],@"PurpleMenuActionData",
2243                                 [NSValue valueWithPointer:buddy],@"PurpleBuddy",
2244                                 nil];
2245                 } else {
2246                         dict = [NSDictionary dictionaryWithObjectsAndKeys:
2247                                 [NSValue valueWithPointer:act->callback],@"PurpleMenuActionCallback",
2248                                 [NSValue valueWithPointer:buddy],@"PurpleBuddy",
2249                                 nil];                   
2250                 }
2251                 
2252                 [menuItem setRepresentedObject:dict];
2253                 
2254                 //If there is a submenu, generate and set it
2255                 if (act->children) {
2256                         NSMutableArray  *childrenArray = [NSMutableArray array];
2257                         GList                   *l, *ll;
2258                         //Add a NSMenuItem for each child
2259                         for (l = ll = act->children; l; l = l->next) {
2260                                 [self addMenuItemForMenuAction:(PurpleMenuAction *)l->data
2261                                                                 forListContact:inContact
2262                                                                          purpleBuddy:buddy
2263                                                                            toArray:childrenArray
2264                                                            withServiceIcon:serviceIcon];
2265                         }
2266                         g_list_free(act->children);
2268                         if ([childrenArray count]) {
2269                                 NSEnumerator *enumerator = [childrenArray objectEnumerator];
2270                                 NSMenuItem       *childMenuItem;
2271                                 NSMenu           *submenu = [[NSMenu alloc] init];
2272                                 
2273                                 while ((childMenuItem = [enumerator nextObject])) {
2274                                         [submenu addItem:childMenuItem];
2275                                 }
2276                                 
2277                                 [menuItem setSubmenu:submenu];
2278                                 [submenu release];
2279                         }
2280                 }
2282                 [menuItemArray addObject:menuItem];
2283                 [menuItem release];
2284         }
2286         purple_menu_action_free(act);
2289 //Returns an array of menuItems specific for this contact based on its account and potentially status
2290 - (NSArray *)menuItemsForContact:(AIListContact *)inContact
2292         NSMutableArray                  *menuItemArray = nil;
2294         if (account && purple_account_is_connected(account)) {
2295                 PurplePlugin                            *prpl;
2296                 PurplePluginProtocolInfo  *prpl_info = ((prpl = purple_find_prpl(account->protocol_id)) ?
2297                                                                                           PURPLE_PLUGIN_PROTOCOL_INFO(prpl) :
2298                                                                                           NULL);
2299                 GList                                   *l, *ll;
2300                 PurpleBuddy                             *buddy;
2301                 
2302                 //Find the PurpleBuddy
2303                 buddy = purple_find_buddy(account, [[inContact UID] UTF8String]);
2304                 
2305                 if (prpl_info && prpl_info->blist_node_menu && buddy) {
2306                         NSImage *serviceIcon = [AIServiceIcons serviceIconForService:[self service]
2307                                                                                                                                         type:AIServiceIconSmall
2308                                                                                                                            direction:AIIconNormal];
2309                         
2310                         menuItemArray = [NSMutableArray array];
2312                         //Add a NSMenuItem for each node action specified by the prpl
2313                         for (l = ll = prpl_info->blist_node_menu((PurpleBlistNode *)buddy); l; l = l->next) {
2314                                 [self addMenuItemForMenuAction:(PurpleMenuAction *)l->data
2315                                                                 forListContact:inContact
2316                                                                          purpleBuddy:buddy
2317                                                                            toArray:menuItemArray
2318                                                            withServiceIcon:serviceIcon];
2319                         }
2320                         g_list_free(ll);
2321                         
2322                         //Don't return an empty array
2323                         if (![menuItemArray count]) menuItemArray = nil;
2324                 }
2325         }
2326         
2327         return menuItemArray;
2330 //Subclasses may override to provide a localized label and/or prevent a specified label from being shown
2331 - (NSString *)titleForContactMenuLabel:(const char *)label forContact:(AIListContact *)inContact
2333         return [NSString stringWithUTF8String:label];
2337 * @brief Menu items for the account's actions
2339  * Returns an array of menu items for account-specific actions.  This is the best place to add protocol-specific
2340  * actions that aren't otherwise supported by Adium.  It will only be queried if the account is online.
2341  * @return NSArray of NSMenuItem instances for this account
2342  */
2343 - (NSArray *)accountActionMenuItems
2345         NSMutableArray                  *menuItemArray = nil;
2346         
2347         if (account && purple_account_is_connected(account)) {
2348                 PurplePlugin *plugin = account->gc->prpl;
2349                 
2350                 if (PURPLE_PLUGIN_HAS_ACTIONS(plugin)) {
2351                         GList   *l, *actions;
2352                         
2353                         actions = PURPLE_PLUGIN_ACTIONS(plugin, account->gc);
2355                         //Avoid adding separators between nonexistant items (i.e. items which Purple shows but we don't)
2356                         BOOL    addedAnAction = NO;
2357                         for (l = actions; l; l = l->next) {
2358                                 
2359                                 if (l->data) {
2360                                         PurplePluginAction      *action;
2361                                         NSDictionary            *dict;
2362                                         NSMenuItem                      *menuItem;
2363                                         NSString                        *title;
2364                                         
2365                                         action = (PurplePluginAction *) l->data;
2366                                         
2367                                         //If titleForAccountActionMenuLabel: returns nil, we don't add the menuItem
2368                                         if (action &&
2369                                                 action->label &&
2370                                                 (title = [self titleForAccountActionMenuLabel:action->label])) {
2372                                                 action->plugin = plugin;
2373                                                 action->context = account->gc;
2375                                                 menuItem = [[[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:title
2376                                                                                                                                                                                  target:self
2377                                                                                                                                                                                  action:@selector(performAccountMenuAction:)
2378                                                                                                                                                                   keyEquivalent:@""] autorelease];
2379                                                 dict = [NSDictionary dictionaryWithObjectsAndKeys:
2380                                                         [NSValue valueWithPointer:action->callback], @"PurplePluginActionCallback",
2381                                                         [NSValue valueWithPointer:action->user_data], @"PurplePluginActionCallbackUserData",
2382                                                         nil];
2383                                                 
2384                                                 [menuItem setRepresentedObject:dict];
2385                                                 
2386                                                 if (!menuItemArray) menuItemArray = [NSMutableArray array];
2387                                                 
2388                                                 [menuItemArray addObject:menuItem];
2389                                                 addedAnAction = YES;
2390                                         } 
2391                                         
2392                                         purple_plugin_action_free(action);
2393                                         
2394                                 } else {
2395                                         if (addedAnAction) {
2396                                                 [menuItemArray addObject:[NSMenuItem separatorItem]];
2397                                                 addedAnAction = NO;
2398                                         }
2399                                 }
2400                         } /* end for */
2401                         
2402                         g_list_free(actions);
2403                 }
2404         }
2406         return menuItemArray;
2409 //Action of a dynamically-generated contact menu item
2410 - (void)performAccountMenuAction:(NSMenuItem *)sender
2412         NSDictionary            *dict = [sender representedObject];
2414         [purpleThread performAccountMenuActionFromDict:dict forAccount:self];
2417 //Subclasses may override to provide a localized label and/or prevent a specified label from being shown
2418 - (NSString *)titleForAccountActionMenuLabel:(const char *)label
2420         if ((strcmp(label, _("Change Password...")) == 0) || (strcmp(label, _("Change Password")) == 0)) {
2421                 return [[NSString stringWithFormat:AILocalizedString(@"Change Password", "Menu item title for changing the password of an account")] stringByAppendingEllipsis];
2422         } else {
2423                 return [NSString stringWithUTF8String:label];
2424         }
2427 /********************************/
2428 /* AIAccount subclassed methods */
2429 /********************************/
2430 #pragma mark AIAccount Subclassed Methods
2431 - (void)initAccount
2433         NSDictionary    *defaults = [NSDictionary dictionaryNamed:[NSString stringWithFormat:@"PurpleDefaults%@",[[self service] serviceID]]
2434                                                                                                          forClass:[self class]];
2435         
2436         if (defaults) {
2437                 [[adium preferenceController] registerDefaults:defaults
2438                                                                                           forGroup:GROUP_ACCOUNT_STATUS
2439                                                                                                 object:self];
2440         } else {
2441                 AILog(@"Failed to load defaults for %@",[NSString stringWithFormat:@"PurpleDefaults%@",[[self service] serviceID]]);
2442         }
2443         
2444         //Defaults
2445         [self setLastDisconnectionError:nil];
2446         
2447         permittedContactsArray = [[NSMutableArray alloc] init];
2448         deniedContactsArray = [[NSMutableArray alloc] init];
2450         //We will create a purpleAccount the first time we attempt to connect
2451         account = NULL;
2453         //Observe preferences changes
2454         [[adium preferenceController] registerPreferenceObserver:self forGroup:PREF_GROUP_ALIASES];
2458  * @brief The account will be deleted, we should ask the user for confirmation. If the prpl supports it, we can also remove
2459  * the account from the server (if the user wants us to do that)
2460  */
2461 - (NSAlert*)alertForAccountDeletion
2463         PurplePlugin *prpl;
2464         PurplePluginProtocolInfo *prpl_info;
2465         
2466         if (!purpleThread) {
2467                 purpleThread = [[SLPurpleCocoaAdapter sharedInstance] retain];  
2468         }       
2469         
2470         prpl = purple_find_prpl([self protocolPlugin]);
2471         if(!prpl)
2472                 return nil;
2473         prpl_info = PURPLE_PLUGIN_PROTOCOL_INFO(prpl);
2474         if(!prpl_info)
2475                 return nil;
2476         if(prpl_info->unregister_user)
2477                 return [NSAlert alertWithMessageText:AILocalizedString(@"Delete Account",nil)
2478                                                            defaultButton:AILocalizedString(@"Delete",nil)
2479                                                          alternateButton:AILocalizedString(@"Cancel",nil)
2480                                                                  otherButton:AILocalizedString(@"Delete & Unregister",nil)
2481                                    informativeTextWithFormat:AILocalizedString(@"Delete the account %@? You can also optionally unregister the account on the server if possible.",nil), ([[self formattedUID] length] ? [self formattedUID] : NEW_ACCOUNT_DISPLAY_TEXT)];
2482         else
2483                 return [super alertForAccountDeletion];
2486 - (void)alertForAccountDeletion:(id<AIAccountControllerRemoveConfirmationDialog>)dialog didReturn:(int)returnCode
2488         PurplePlugin *prpl;
2489         PurplePluginProtocolInfo *prpl_info;
2490         
2491         if (!purpleThread) {
2492                 purpleThread = [[SLPurpleCocoaAdapter sharedInstance] retain];  
2493         }       
2494         
2495         prpl = purple_find_prpl([self protocolPlugin]);
2496         if(!prpl) {
2497                 [super alertForAccountDeletion:dialog didReturn:NSAlertAlternateReturn];
2498                 return;
2499         }
2501         prpl_info = PURPLE_PLUGIN_PROTOCOL_INFO(prpl);
2502         if(!prpl_info) {
2503                 [super alertForAccountDeletion:dialog didReturn:NSAlertAlternateReturn];
2504                 return;
2505         }
2507         /* If the user canceled, we can tell the superclass immediately.
2508          * If the deletion is in fact happening, we first have to unregister and disconnect.
2509          * This is an asynchronous process.
2510          */
2511         if(prpl_info->unregister_user) {
2512                 switch(returnCode) {
2513                         case NSAlertOtherReturn: // delete & unregister
2514                                 deletionDialog = dialog;
2515                                 if(!account || !purple_account_is_connected(account)) {
2516                                         password = [[[adium accountController] passwordForAccount:self] retain];
2517                                         [self connect];
2518                                 } else
2519                                         [purpleThread unregisterAccount:self];
2520                                 // further progress happens in -unregisteredAccount:
2521                                 break;
2522                         case NSAlertDefaultReturn: // delete
2523                                 willBeDeleted = YES;
2524                                 if(!account || !purple_account_is_connected(account)) {
2525                                         [super alertForAccountDeletion:dialog didReturn:NSAlertDefaultReturn];
2526                                 } else {
2527                                         deletionDialog = dialog;
2528                                         [self setShouldBeOnline:NO];
2529                                         // further progress happens in -accountConnectionDisconnected
2530                                 }
2531                                 break;
2532                         default: // cancel
2533                                 [super alertForAccountDeletion:dialog didReturn:NSAlertAlternateReturn];
2534                 }
2536         } else {
2537                 switch(returnCode) {
2538                         case NSAlertDefaultReturn:
2539                                 willBeDeleted = YES;
2540                                 if (!account || !purple_account_is_connected(account)) {
2541                                         [super alertForAccountDeletion:dialog didReturn:NSAlertDefaultReturn];
2542                                 } else {
2543                                         deletionDialog = dialog;
2544                                         [self setShouldBeOnline:NO];
2545                                         // further progress happens in -accountConnectionDisconnected
2546                                 }
2547                                 break;
2548                         default:
2549                                 [super alertForAccountDeletion:dialog didReturn:returnCode];
2550                 }
2551         }
2554 - (void)unregisteredAccount:(BOOL)success {
2555         if(success) {
2556                 willBeDeleted = YES;
2557                 NSInvocation *inv = [NSInvocation invocationWithMethodSignature:[self methodSignatureForSelector:@selector(setShouldBeOnline:)]];
2558                 [inv setTarget:self];
2559                 [inv setSelector:@selector(setShouldBeOnline:)];
2560                 static BOOL nope = NO;
2561                 [inv setArgument:&nope atIndex:2];
2562                 [inv performSelector:@selector(invoke) withObject:nil afterDelay:0.0];
2563                 // further progress happens in -accountConnectionDisconnected
2565         } else {
2566                 [super alertForAccountDeletion:deletionDialog didReturn:NSAlertAlternateReturn];
2567                 deletionDialog = nil;
2568         }
2572 * @brief The account's UID changed
2573  */
2574 - (void)didChangeUID
2576         //Only need to take action if we have a created PurpleAccount already
2577         if (account != NULL) {
2578                 //Remove our current account
2579                 [purpleThread removeAdiumAccount:self];
2580                 
2581                 //Clear the reference to the PurpleAccount... it'll be created when needed
2582                 account = NULL;
2583         }
2586 - (void)dealloc
2587 {       
2588         [[adium preferenceController] unregisterPreferenceObserver:self];
2590         [permittedContactsArray release];
2591         [deniedContactsArray release];
2592         
2593     [super dealloc];
2596 - (NSString *)unknownGroupName {
2597     return (@"Unknown");
2600 - (NSDictionary *)defaultProperties { return [NSDictionary dictionary]; }
2602 - (NSString *)encodedAttributedString:(NSAttributedString *)inAttributedString forStatusState:(AIStatus *)statusState
2604         return [self encodedAttributedString:inAttributedString forListObject:nil];     
2607 - (void)preferencesChangedForGroup:(NSString *)group key:(NSString *)key
2608                                                         object:(AIListObject *)object preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime
2610         [super preferencesChangedForGroup:group key:key object:object preferenceDict:prefDict firstTime:firstTime];
2612         if ([group isEqualToString:PREF_GROUP_ALIASES]) {
2613                 //If the notification object is a listContact belonging to this account, update the serverside information
2614                 if ((account != nil) && 
2615                         ([self shouldSetAliasesServerside]) &&
2616                         ([key isEqualToString:@"Alias"])) {
2618                         NSString *alias = [object preferenceForKey:@"Alias"
2619                                                                                                  group:PREF_GROUP_ALIASES 
2620                                                                  ignoreInheritedValues:YES];
2622                         if ([object isKindOfClass:[AIMetaContact class]]) {
2623                                 NSEnumerator    *enumerator = [[(AIMetaContact *)object containedObjects] objectEnumerator];
2624                                 AIListContact   *containedListContact;
2625                                 while ((containedListContact = [enumerator nextObject])) {
2626                                         if ([containedListContact account] == self) {
2627                                                 [purpleThread setAlias:alias forUID:[containedListContact UID] onAccount:self];
2628                                         }
2629                                 }
2630                                 
2631                         } else if ([object isKindOfClass:[AIListContact class]]) {
2632                                 if ([(AIListContact *)object account] == self) {
2633                                         [purpleThread setAlias:alias forUID:[object UID] onAccount:self];
2634                                 }
2635                         }
2636                 }
2637         }
2640 #pragma mark Actions for chats
2643  * @name Actions for chats
2644  * @brief This method returns an NSMenu containing the 
2645  * actions that are allowed for a given chat.
2646  * @params An AIChat for which the commands are fetched
2647  * @return NSMenu with the proper commands
2648  */
2649 - (NSMenu*)actionsForChat:(AIChat*)chat
2651         NSMenu *actionsMenu = [[NSMenu alloc] initWithTitle:@"commandsmenu"];
2652         PurpleConversation *conv = existingConvLookupFromChat(chat);
2654         GList *list = purple_cmd_list(conv);
2655         GList *l;
2657         for (l = list; l != NULL; l = l->next) {
2658                 const char  *cmdName = l->data;
2660                 GList           *cmdDescription = purple_cmd_help(conv, cmdName);
2661                 NSString        *name = [NSString stringWithUTF8String:cmdName];
2662                 NSString *menuTitle;
2663                 if (cmdDescription && cmdDescription->data)
2664                         menuTitle = [[AIHTMLDecoder decodeHTML:[NSString stringWithUTF8String:cmdDescription->data]] string];
2665                 else
2666                         menuTitle = name;
2668                 [actionsMenu addItemWithTitle:menuTitle
2669                                                            target:self
2670                                                            action:@selector(doCommand:)
2671                                                 keyEquivalent:@""
2672                                         representedObject:[NSDictionary dictionaryWithObjectsAndKeys:
2673                                                                            chat, @"associatedChat",
2674                                                                            name, @"commandName",
2675                                                                            nil]];
2676         } 
2678         g_list_free(list);
2680         return [actionsMenu autorelease]; 
2683 -(void)doCommand:(id)sender
2685         NSDictionary *dict = [sender representedObject];
2686         [self verifyCommand:[dict objectForKey:@"commandName"]
2687                                 forChat:[dict objectForKey:@"associatedChat"]];
2690 -(void)executeCommandWithParameters:(NSMutableDictionary*)parameters
2692         BOOL result = [purpleThread doCommand:[parameters objectForKey:@"totalCommandString"] fromAccount:[parameters objectForKey:@"account"] inChat:[parameters objectForKey:@"chat"]];
2693         if(result == FALSE)     {
2694 #warning Incomplete
2695                 int choice = NSRunAlertPanel(@"Command Failed!",@"command failed: %@ from Account: %@ in Chat: %@",@"Cancel",@"OK",nil,[parameters objectForKey:@"totalCommandString"],[parameters objectForKey:@"account"],[parameters objectForKey:@"chat"]); 
2696         }
2700 - (BOOL)validateMenuItem:(NSMenuItem *)item 
2702         return YES;
2706 /***************************/
2707 /* Account private methods */
2708 /***************************/
2709 #pragma mark Private
2710 - (void)setTypingFlagOfChat:(AIChat *)chat to:(NSNumber *)typingStateNumber
2712     AITypingState currentTypingState = [chat integerStatusObjectForKey:KEY_TYPING];
2713         AITypingState newTypingState = [typingStateNumber intValue];
2715     if (currentTypingState != newTypingState) {
2716                 [chat setStatusObject:(newTypingState ? typingStateNumber : nil)
2717                                            forKey:KEY_TYPING
2718                                            notify:NotifyNow];
2719     }
2722 - (NSNumber *)shouldCheckMail
2724         return [self preferenceForKey:KEY_ACCOUNT_CHECK_MAIL group:GROUP_ACCOUNT_STATUS];
2727 - (BOOL)shouldSetAliasesServerside
2729         return NO;
2732 - (NSString *)internalObjectID
2734         return [super internalObjectID];
2738 @end